initial commit

This commit is contained in:
boris
2025-09-30 09:24:25 +01:00
committed by boris
parent a783a12c97
commit c7770ea03b
4695 changed files with 525784 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% set icon %}
{{ source('@WebProfiler/Icon/ajax.svg') }}
<span class="sf-toolbar-value sf-toolbar-ajax-request-counter">0</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<span class="sf-toolbar-header">
<b class="sf-toolbar-ajax-info"></b>
<b class="sf-toolbar-action">(<a class="sf-toolbar-ajax-clear" href="javascript:void(0);">Clear</a>)</b>
</span>
</div>
<div class="sf-toolbar-info-piece">
<table class="sf-toolbar-ajax-requests">
<thead>
<tr>
<th>#</th>
<th>Profile</th>
<th>Method</th>
<th>Type</th>
<th>Status</th>
<th>URL</th>
<th>Time</th>
</tr>
</thead>
<tbody class="sf-toolbar-ajax-request-list"></tbody>
</table>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: false }) }}
{% endblock %}

View File

@@ -0,0 +1,178 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% if collector.totals.calls > 0 %}
{% set icon %}
{{ source('@WebProfiler/Icon/cache.svg') }}
<span class="sf-toolbar-value">{{ collector.totals.calls }}</span>
<span class="sf-toolbar-info-piece-additional-detail">
<span class="sf-toolbar-label">in</span>
<span class="sf-toolbar-value">{{ '%0.2f'|format(collector.totals.time * 1000) }}</span>
<span class="sf-toolbar-label">ms</span>
</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Cache Calls</b>
<span>{{ collector.totals.calls }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Total time</b>
<span>{{ '%0.2f'|format(collector.totals.time * 1000) }} ms</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Cache hits</b>
<span>{{ collector.totals.hits }} / {{ collector.totals.reads }}{% if collector.totals.hit_read_ratio is not null %} ({{ collector.totals.hit_read_ratio }}%){% endif %}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Cache writes</b>
<span>{{ collector.totals.writes }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label {{ collector.totals.calls == 0 ? 'disabled' }}">
<span class="icon">
{{ source('@WebProfiler/Icon/cache.svg') }}
</span>
<strong>Cache</strong>
</span>
{% endblock %}
{% block panel %}
<h2>Cache</h2>
{% if collector.totals.calls == 0 %}
<div class="empty empty-panel">
<p>No cache calls were made.</p>
</div>
{% else %}
{{ _self.render_metrics(collector.totals, true) }}
<h2>Pools</h2>
<div class="sf-tabs">
{# the empty merge is needed to turn the iterator into an array #}
{% set cache_pools_with_calls = collector.calls|filter(calls => calls|length > 0)|merge([]) %}
{% for name, calls in cache_pools_with_calls %}
<div class="tab {{ calls|length == 0 ? 'disabled' }}">
<h3 class="tab-title">{{ name }} <span class="badge">{{ collector.statistics[name].calls }}</span></h3>
<div class="tab-content">
<h4>Adapter</h4>
<div class="card">
{% if collector.adapters[name] is defined %}
<code>{{ collector.adapters[name] }}</code>
{% else %}
<span class="text-muted">Unable to get the adapter class.</span>
{% endif %}
</div>
{% if calls|length == 0 %}
<div class="empty">
<p>No calls were made for {{ name }} pool.</p>
</div>
{% else %}
<h4>Metrics</h4>
{{ _self.render_metrics(collector.statistics[name]) }}
<h4>Calls</h4>
<table>
<thead>
<tr>
<th>#</th>
<th>Time</th>
<th>Call</th>
<th>Hit</th>
</tr>
</thead>
<tbody>
{% for call in calls %}
<tr>
<td class="font-normal text-small text-muted nowrap">{{ loop.index }}</td>
<td class="nowrap">{{ '%0.2f'|format((call.end - call.start) * 1000) }} ms</td>
<td class="nowrap">{{ call.name }}({{ call.namespace|default('') }})</td>
<td>{{ profiler_dump(call.value.result, maxDepth=2) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
{% if loop.last %}
<div class="tab">
<h3 class="tab-title">Pools without calls <span class="badge">{{ collector.calls|filter(calls => 0 == calls|length)|length }}</span></h3>
<div class="tab-content">
<table>
<thead>
<tr>
<th>Cache pools that received no calls</th>
</tr>
</thead>
<tbody>
{% for cache_pool in collector.calls|filter(calls => 0 == calls|length)|keys|sort %}
<tr><td>{{ cache_pool }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% endblock %}
{% macro render_metrics(pool, is_total = false) %}
<div class="metrics">
<div class="metric">
<span class="value">{{ pool.calls }}</span>
<span class="label">{{ is_total ? 'Total calls' : 'Calls' }}</span>
</div>
<div class="metric">
<span class="value">{{ '%0.2f'|format(pool.time * 1000) }} <span class="unit">ms</span></span>
<span class="label">{{ is_total ? 'Total time' : 'Time' }}</span>
</div>
<div class="metric-divider"></div>
<div class="metric-group">
<div class="metric">
<span class="value">{{ pool.reads }}</span>
<span class="label">{{ is_total ? 'Total reads' : 'Reads' }}</span>
</div>
<div class="metric">
<span class="value">{{ pool.writes }}</span>
<span class="label">{{ is_total ? 'Total writes' : 'Writes' }}</span>
</div>
<div class="metric">
<span class="value">{{ pool.deletes }}</span>
<span class="label">{{ is_total ? 'Total deletes' : 'Deletes' }}</span>
</div>
</div>
<div class="metric-divider"></div>
<div class="metric-group">
<div class="metric">
<span class="value">{{ pool.hits }}</span>
<span class="label">{{ is_total ? 'Total hits' : 'Hits' }}</span>
</div>
<div class="metric">
<span class="value">{{ pool.misses }}</span>
<span class="label">{{ is_total ? 'Total misses' : 'Misses' }}</span>
</div>
<div class="metric">
<span class="value">
{{ pool.hit_read_ratio ?? 0 }} <span class="unit">%</span>
</span>
<span class="label">Hits/reads</span>
</div>
</div>
</div>
{% endmacro %}

View File

@@ -0,0 +1,249 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block menu %}
<span class="label">
<span class="icon">{{ source('@WebProfiler/Icon/command.svg') }}</span>
<strong>Console Command</strong>
</span>
{% endblock %}
{% block panel %}
<h2>
{% set command = collector.command %}
<a href="{{ command.file|file_link(command.line) }}">
{% if command.executor is defined %}
{{ command.executor|abbr_method }}
{% else %}
{{ command.class|abbr_class }}
{% endif %}
</a>
</h2>
<div class="sf-tabs">
<div class="tab">
<h3 class="tab-title">Command</h3>
<div class="tab-content">
<div class="metrics">
<div class="metric">
<span class="value">{{ collector.duration }}</span>
<span class="label">Duration</span>
</div>
<div class="metric">
<span class="value">{{ collector.maxMemoryUsage }}</span>
<span class="label">Peak Memory Usage</span>
</div>
<div class="metric">
<span class="value">{{ collector.verbosityLevel }}</span>
<span class="label">Verbosity Level</span>
</div>
</div>
<div class="metrics">
<div class="metric">
<span class="value">{{ source('@WebProfiler/Icon/' ~ (collector.signalable is not empty ? 'yes' : 'no') ~ '.svg') }}</span>
<span class="label">Signalable</span>
</div>
<div class="metric">
<span class="value">{{ source('@WebProfiler/Icon/' ~ (collector.interactive ? 'yes' : 'no') ~ '.svg') }}</span>
<span class="label">Interactive</span>
</div>
<div class="metric">
<span class="value">{{ source('@WebProfiler/Icon/' ~ (collector.validateInput ? 'yes' : 'no') ~ '.svg') }}</span>
<span class="label">Validate Input</span>
</div>
<div class="metric">
<span class="value">{{ source('@WebProfiler/Icon/' ~ (collector.enabled ? 'yes' : 'no') ~ '.svg') }}</span>
<span class="label">Enabled</span>
</div>
<div class="metric">
<span class="value">{{ source('@WebProfiler/Icon/' ~ (collector.visible ? 'yes' : 'no') ~ '.svg') }}</span>
<span class="label">Visible</span>
</div>
</div>
<h3>Arguments</h3>
{% if collector.arguments is empty %}
<div class="empty">
<p>No arguments were set</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/table.html.twig', { data: collector.arguments, labels: ['Argument', 'Value'], maxDepth: 2 }, with_context=false) }}
{% endif %}
<h3>Options</h3>
{% if collector.options is empty %}
<div class="empty">
<p>No options were set</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/table.html.twig', { data: collector.options, labels: ['Option', 'Value'], maxDepth: 2 }, with_context=false) }}
{% endif %}
{% if collector.interactive %}
<h3>Interactive Inputs</h3>
<p class="help">
The values which have been set interactively.
</p>
{% if collector.interactiveInputs is empty %}
<div class="empty">
<p>No inputs were set</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/table.html.twig', { data: collector.interactiveInputs, labels: ['Input', 'Value'], maxDepth: 2 }, with_context=false) }}
{% endif %}
{% endif %}
<h3>Application inputs</h3>
{% if collector.applicationInputs is empty %}
<div class="empty">
<p>No application inputs are set</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/table.html.twig', { data: collector.applicationInputs, labels: ['Input', 'Value'], maxDepth: 2 }, with_context=false) }}
{% endif %}
</div>
</div>
<div class="tab">
<h3 class="tab-title">Input / Output</h3>
<div class="tab-content">
<table>
<tr>
<td class="font-normal">Input</td>
<td class="font-normal">{{ profiler_dump(collector.input) }}</td>
</tr>
<tr>
<td class="font-normal">Output</td>
<td class="font-normal">{{ profiler_dump(collector.output) }}</td>
</tr>
</table>
</div>
</div>
<div class="tab">
<h3 class="tab-title">Helper Set</h3>
<div class="tab-content">
{% if collector.helperSet is empty %}
<div class="empty">
<p>No helpers</p>
</div>
{% else %}
<table class="{{ class|default('') }}">
<thead>
<tr>
<th scope="col">Helpers</th>
</tr>
</thead>
<tbody>
{% for helper in collector.helperSet|sort %}
<tr>
<td>{{ profiler_dump(helper) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
<div class="tab">
{% set request_collector = profile.collectors.request %}
<h3 class="tab-title">Server Parameters</h3>
<div class="tab-content">
<h3>Server Parameters</h3>
<h4>Defined in .env</h4>
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: request_collector.dotenvvars }, with_context = false) }}
<h4>Defined as regular env variables</h4>
{% set requestserver = [] %}
{% for key, value in request_collector.requestserver|filter((_, key) => key not in request_collector.dotenvvars.keys) %}
{% set requestserver = requestserver|merge({(key): value}) %}
{% endfor %}
{{ include('@WebProfiler/Profiler/table.html.twig', { data: requestserver }, with_context = false) }}
</div>
</div>
{% if collector.signalable is not empty %}
<div class="tab">
<h3 class="tab-title">Signals</h3>
<div class="tab-content">
<h3>Subscribed signals</h3>
{{ collector.signalable|join(', ') }}
<h3>Handled signals</h3>
{% if collector.handledSignals is empty %}
<div class="empty">
<p>No signals handled</p>
</div>
{% else %}
<table>
<thead>
<tr>
<th>Signal</th>
<th>Times handled</th>
<th>Total execution time</th>
<th>Memory peak</th>
</tr>
</thead>
<tbody>
{% for signal, data in collector.handledSignals %}
<tr>
<td>{{ signal }}</td>
<td>{{ data.handled }}</td>
<td>{{ data.duration }} ms</td>
<td>{{ data.memory }} MiB</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
{% endif %}
{% if profile.parent %}
<div class="tab">
<h3 class="tab-title">Parent Command</h3>
<div class="tab-content">
<h3>
<a href="{{ path('_profiler', { token: profile.parent.token }) }}">Return to parent command</a>
<small>(token = {{ profile.parent.token }})</small>
</h3>
{{ profile.parent.url }}
</div>
</div>
{% endif %}
{% if profile.children|length %}
<div class="tab">
<h3 class="tab-title">Sub Commands <span class="badge">{{ profile.children|length }}</span></h3>
<div class="tab-content">
{% for child in profile.children %}
<h3>
{{ child.url }}
<small>(token = <a href="{{ path('_profiler', { token: child.token }) }}">{{ child.token }}</a>)</small>
</h3>
{% endfor %}
</div>
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,300 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block head %}
{{ parent() }}
<style>
.config-symfony-version-lts {
border: 0;
color: var(--color-muted);
font-size: 21px;
line-height: 33px;
}
.config-symfony-version-lts[title] {
text-decoration: none;
}
.config-symfony-version-status-badge {
background-color: var(--badge-background);
border-radius: 4px;
color: var(--badge-color);
display: inline-block;
font-size: 15px;
font-weight: bold;
margin: 10px 0 5px;
padding: 3px 7px;
white-space: nowrap;
}
.config-symfony-version-status-badge.status-success {
background-color: var(--badge-success-background);
color: var(--badge-success-color);
}
.config-symfony-version-status-badge.status-warning {
background-color: var(--badge-warning-background);
color: var(--badge-warning-color);
}
.config-symfony-version-status-badge.status-error {
background-color: var(--badge-danger-background);
color: var(--badge-danger-color);
}
.config-symfony-version-roadmap-link {
display: inline-block;
margin: 10px 5px 5px;
}
.config-symfony-eol {
margin-top: 5px;
}
</style>
{% endblock %}
{% block toolbar %}
{% if 'unknown' == collector.symfonyState %}
{% set block_status = '' %}
{% set symfony_version_status = 'Unable to retrieve information about the Symfony version.' %}
{% elseif 'eol' == collector.symfonyState %}
{% set block_status = 'red' %}
{% set symfony_version_status = 'This Symfony version will no longer receive security fixes.' %}
{% elseif 'eom' == collector.symfonyState %}
{% set block_status = 'yellow' %}
{% set symfony_version_status = 'This Symfony version will only receive security fixes.' %}
{% elseif 'dev' == collector.symfonyState %}
{% set block_status = 'yellow' %}
{% set symfony_version_status = 'This Symfony version is still in the development phase.' %}
{% else %}
{% set block_status = '' %}
{% set symfony_version_status = '' %}
{% endif %}
{% set icon %}
<span class="sf-toolbar-label">
{{ source('@WebProfiler/Icon/symfony.svg') }}
</span>
<span class="sf-toolbar-value">{{ collector.symfonyState is defined ? collector.symfonyversion : 'n/a' }}</span>
{% endset %}
{% set text %}
{% if symfony_version_status %}
<div class="sf-toolbar-info-group">
<div class="sf-toolbar-info-piece">
<span>{{ symfony_version_status }}</span>
</div>
</div>
{% endif %}
<div class="sf-toolbar-info-group">
<div class="sf-toolbar-info-piece">
<b>Profiler token</b>
<span>
{% if profiler_url %}
<a href="{{ profiler_url }}">{{ collector.token }}</a>
{% else %}
{{ collector.token }}
{% endif %}
</span>
</div>
{% if 'n/a' is not same as(collector.env) %}
<div class="sf-toolbar-info-piece">
<b>Environment</b>
<span>{{ collector.env }}</span>
</div>
{% endif %}
{% if 'n/a' is not same as(collector.debug) %}
<div class="sf-toolbar-info-piece">
<b>Debug</b>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.debug ? 'green' : 'red' }}">{{ collector.debug ? 'enabled' : 'disabled' }}</span>
</div>
{% endif %}
</div>
<div class="sf-toolbar-info-group">
<div class="sf-toolbar-info-piece sf-toolbar-info-php">
<b>PHP version</b>
<span{% if collector.phpversionextra %} title="{{ collector.phpversion ~ collector.phpversionextra }}"{% endif %}>
{{ collector.phpversion }}
&nbsp; <a href="{{ path('_profiler_phpinfo') }}">View phpinfo()</a>
</span>
</div>
<div class="sf-toolbar-info-piece sf-toolbar-info-php-ext">
<b>PHP Extensions</b>
{% if collector.hasXdebugInfo %}
<a href="{{ path('_profiler_xdebug') }}" title="View xdebug_info()">
{% endif %}
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.hasXdebug ? 'green' : 'gray' }}">Xdebug {{ collector.hasXdebug ? '✓' : '✗' }}</span>
{% if collector.hasXdebugInfo %}
</a>
{% endif %}
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.hasapcu ? 'green' : 'gray' }}">APCu {{ collector.hasapcu ? '✓' : '✗' }}</span>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.haszendopcache ? 'green' : 'red' }}">OPcache {{ collector.haszendopcache ? '✓' : '✗' }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>PHP SAPI</b>
<span>{{ collector.sapiName }}</span>
</div>
</div>
<div class="sf-toolbar-info-group">
{% if collector.symfonyversion is defined %}
<div class="sf-toolbar-info-piece">
<b>Resources</b>
<span>
<a href="https://symfony.com/doc/{{ collector.symfonyversion }}/index.html" rel="help">
Read Symfony {{ collector.symfonyversion }} Docs
</a>
</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Help</b>
<span>
<a href="https://symfony.com/support">
Symfony Support Channels
</a>
</span>
</div>
{% endif %}
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: true, name: 'config', status: block_status, additional_classes: 'sf-toolbar-block-right' }) }}
{% endblock %}
{% block menu %}
<span class="label label-status-{{ collector.symfonyState == 'eol' ? 'red' : collector.symfonyState in ['eom', 'dev'] ? 'yellow' }}">
<span class="icon">{{ source('@WebProfiler/Icon/config.svg') }}</span>
<strong>Configuration</strong>
</span>
{% endblock %}
{% block panel %}
<h2>Symfony Configuration</h2>
<div class="metrics">
<div class="metric">
<span class="value">
{{ collector.symfonyversion }}
{% if collector.symfonylts %}
<abbr class="config-symfony-version-lts" title="This is a Long-Term Support version">(LTS)</abbr>
{% endif %}
</span>
<span class="label">Symfony version</span>
</div>
{% if 'n/a' is not same as(collector.env) %}
<div class="metric">
<span class="value">{{ collector.env }}</span>
<span class="label">Environment</span>
</div>
{% endif %}
{% if 'n/a' is not same as(collector.debug) %}
<div class="metric">
<span class="value">{{ collector.debug ? 'enabled' : 'disabled' }}</span>
<span class="label">Debug</span>
</div>
{% endif %}
</div>
{% set symfony_status = { dev: 'In Development', stable: 'Maintained', eom: 'Security Fixes Only', eol: 'Unmaintained' } %}
{% set symfony_status_class = { dev: 'warning', stable: 'success', eom: 'warning', eol: 'error' } %}
<div class="metrics">
<div class="metric-group">
<div class="metric">
<span class="value">
<span class="config-symfony-version-status-badge status-{{ symfony_status_class[collector.symfonystate] }}">{{ symfony_status[collector.symfonystate]|upper }}</span>
</span>
<span class="label">Your Symfony version status</span>
</div>
{% if collector.symfonylts %}
<div class="metric">
<span class="value config-symfony-eol">
{{ collector.symfonyeom }}
</span>
<span class="label">Bug fixes {{ collector.symfonystate in ['eom', 'eol'] ? 'ended on' : 'until' }}</span>
</div>
{% endif %}
<div class="metric">
<span class="value config-symfony-eol">
{{ collector.symfonyeol }}
</span>
<span class="label">
{{ collector.symfonylts ? 'Security fixes' : 'Bug fixes and security fixes' }}
{{ 'eol' == collector.symfonystate ? 'ended on' : 'until' }}</span>
</div>
</div>
</div>
<a class="config-symfony-version-roadmap-link" href="https://symfony.com/releases/{{ collector.symfonyminorversion }}">View Symfony {{ collector.symfonyversion }} release details</a>
<h2>PHP Configuration</h2>
<div class="metrics">
<div class="metric">
<span class="value">{{ collector.phpversion }}{% if collector.phpversionextra %} <span class="unit">{{ collector.phpversionextra }}</span>{% endif %}</span>
<span class="label">PHP version</span>
</div>
<div class="metric">
<span class="value">{{ collector.phparchitecture }} <span class="unit">bits</span></span>
<span class="label">Architecture</span>
</div>
<div class="metric">
<span class="value">{{ collector.phpintllocale }}</span>
<span class="label">Intl locale</span>
</div>
<div class="metric">
<span class="value">{{ collector.phptimezone }}</span>
<span class="label">Timezone</span>
</div>
</div>
<div class="metrics">
<div class="metric-group">
<div class="metric">
<span class="value value-is-icon {{ not collector.haszendopcache ? 'value-shows-no-color' }}" title="{{ collector.zendopcachestatus|default('') }}">{{ source('@WebProfiler/Icon/' ~ (collector.haszendopcache ? 'yes' : 'no') ~ '.svg') }}</span>
<span class="label">OPcache</span>
</div>
<div class="metric">
<span class="value value-is-icon {{ not collector.hasapcu ? 'value-shows-no-color' }}" title="{{ collector.apcustatus|default('') }}">{{ source('@WebProfiler/Icon/' ~ (collector.hasapcu ? 'yes' : 'no') ~ '.svg') }}</span>
<span class="label">APCu</span>
</div>
<div class="metric">
<span class="value value-is-icon {{ not collector.hasxdebug ? 'value-shows-no-color' }}" title="{{ collector.xdebugstatus|default('') }}">{{ source('@WebProfiler/Icon/' ~ (collector.hasxdebug ? 'yes' : 'no') ~ '.svg') }}</span>
<span class="label">Xdebug</span>
</div>
</div>
</div>
<p>
<a href="{{ path('_profiler_phpinfo') }}">View full PHP configuration</a>
</p>
{% if collector.bundles %}
<h2>Enabled Bundles <small>({{ collector.bundles|length }})</small></h2>
<table>
<thead>
<tr>
<th class="key">Name</th>
<th>Class</th>
</tr>
</thead>
<tbody>
{% for name in collector.bundles|keys|sort %}
<tr>
<th scope="row" class="font-normal">{{ name }}</th>
<td class="font-normal">{{ profiler_dump(collector.bundles[name]) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,126 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block menu %}
<span class="label">
<span class="icon">{{ source('@WebProfiler/Icon/event.svg') }}</span>
<strong>Events</strong>
</span>
{% endblock %}
{% block panel %}
<h2>Dispatched Events</h2>
<div class="sf-tabs">
{% for dispatcherName, dispatcherData in collector.data %}
<div class="tab">
<h3 class="tab-title">{{ dispatcherName }}</h3>
<div class="tab-content">
{% if dispatcherData['called_listeners'] is empty %}
<div class="empty empty-panel">
<p>No events have been recorded. Check that debugging is enabled in the kernel.</p>
</div>
{% else %}
<div class="sf-tabs">
<div class="tab">
<h3 class="tab-title">Called Listeners <span class="badge">{{ dispatcherData['called_listeners']|length }}</span></h3>
<div class="tab-content">
{{ _self.render_table(dispatcherData['called_listeners']) }}
</div>
</div>
<div class="tab">
<h3 class="tab-title">Not Called Listeners <span class="badge">{{ dispatcherData['not_called_listeners']|length }}</span></h3>
<div class="tab-content">
{% if dispatcherData['not_called_listeners'] is empty %}
<div class="empty">
<p>
<strong>There are no uncalled listeners</strong>.
</p>
<p>
All listeners were called or an error occurred
when trying to collect uncalled listeners (in which case check the
logs to get more information).
</p>
</div>
{% else %}
{{ _self.render_table(dispatcherData['not_called_listeners']) }}
{% endif %}
</div>
</div>
<div class="tab">
<h3 class="tab-title">Orphaned Events <span class="badge">{{ dispatcherData['orphaned_events']|length }}</span></h3>
<div class="tab-content">
{% if dispatcherData['orphaned_events'] is empty %}
<div class="empty">
<p>
<strong>There are no orphaned events</strong>.
</p>
<p>
All dispatched events were handled or an error occurred
when trying to collect orphaned events (in which case check the
logs to get more information).
</p>
</div>
{% else %}
<table>
<thead>
<tr>
<th>Event</th>
</tr>
</thead>
<tbody>
{% for event in dispatcherData['orphaned_events'] %}
<tr>
<td class="font-normal">{{ event }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% endblock %}
{% macro render_table(listeners) %}
<table>
<thead>
<tr>
<th class="text-right">Priority</th>
<th>Listener</th>
</tr>
</thead>
{% set previous_event = (listeners|first).event %}
{% for listener in listeners %}
{% if loop.first or listener.event != previous_event %}
{% if not loop.first %}
</tbody>
{% endif %}
<tbody>
<tr>
<th colspan="2" class="colored font-normal">{{ listener.event }}</th>
</tr>
{% set previous_event = listener.event %}
{% endif %}
<tr>
<td class="text-right nowrap">{{ listener.priority|default('-') }}</td>
<td class="font-normal">{{ profiler_dump(listener.stub) }}</td>
</tr>
{% if loop.last %}
</tbody>
{% endif %}
{% endfor %}
</table>
{% endmacro %}

View File

@@ -0,0 +1,32 @@
.container {
max-width: none;
margin: 0;
padding: 0;
}
.container .container {
padding: 0;
}
.exception-summary {
background: var(--base-0);
border: var(--border);
box-shadow: 0 0 1px rgba(128, 128, 128, .2);
margin: 1em 0;
padding: 10px;
}
.exception-summary.exception-without-message {
display: none;
}
.exception-message {
color: var(--color-error);
}
.exception-metadata,
.exception-illustration {
display: none;
}
.exception-message-wrapper .container {
min-height: unset;
}

View File

@@ -0,0 +1,45 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block head %}
{% if collector.hasexception %}
<style>
{{ render(controller('web_profiler.controller.exception_panel::stylesheet', { token: token })) }}
{{ include('@WebProfiler/Collector/exception.css.twig') }}
</style>
{% endif %}
{{ parent() }}
{% endblock %}
{% block menu %}
<span class="label {{ collector.hasexception ? 'label-status-error' : 'disabled' }}">
<span class="icon">{{ source('@WebProfiler/Icon/exception.svg') }}</span>
<strong>Exception</strong>
{% if collector.hasexception %}
<span class="count">
<span>1</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
{# these styles are needed to override some styles from Exception page, which wasn't
updated yet to the new style of the Symfony Profiler #}
<style>
.tab-navigation li { background: none; border: 0; font-size: 14px; }
.tab-navigation li.active { border-radius: 6px; }
.tab-navigation li.active .badge { background-color: var(--selected-badge-background); color: var(--selected-badge-color); }
</style>
<h2>Exceptions</h2>
{% if not collector.hasexception %}
<div class="empty empty-panel">
<p>No exception was thrown and caught.</p>
</div>
{% else %}
<div class="sf-reset">
{{ render(controller('web_profiler.controller.exception_panel::body', { token: token })) }}
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,706 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% if collector.data.nb_errors > 0 or collector.data.forms|length %}
{% set status_color = collector.data.nb_errors ? 'red' %}
{% set icon %}
{{ source('@WebProfiler/Icon/form.svg') }}
<span class="sf-toolbar-value">
{{ collector.data.nb_errors ?: collector.data.forms|length }}
</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Number of forms</b>
<span class="sf-toolbar-status">{{ collector.data.forms|length }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Number of errors</b>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.data.nb_errors > 0 ? 'red' }}">{{ collector.data.nb_errors }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label label-status-{{ collector.data.nb_errors ? 'error' }} {{ collector.data.forms is empty ? 'disabled' }}">
<span class="icon">{{ source('@WebProfiler/Icon/form.svg') }}</span>
<strong>Forms</strong>
{% if collector.data.nb_errors > 0 %}
<span class="count">
<span>{{ collector.data.nb_errors }}</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block stylesheets %}
{{ parent() }}
<style>
.form-type-class {
font-size: var(--font-size-body);
display: flex;
margin: 0 0 15px;
}
.form-type-class-label {
margin-right: 4px;
}
.form-type-class pre.sf-dump {
font-family: var(--font-family-system) !important;
font-size: var(--font-size-body) !important;
margin-left: 5px;
}
.form-type-class .sf-dump .sf-dump-str {
color: var(--color-link) !important;
text-decoration: underline;
}
.form-type-class .sf-dump .sf-dump-str:hover {
text-decoration: none;
}
#tree-menu {
float: left;
padding-right: 10px;
width: 220px;
}
#tree-menu ul {
list-style: none;
margin: 0;
padding-left: 0;
}
#tree-menu li {
margin: 0;
padding: 0;
width: 100%;
}
#tree-menu .empty {
border: 0;
box-shadow: none !important;
padding: 0;
}
#tree-details-container {
border-left: 1px solid var(--table-border-color);
margin-left: 230px;
padding-left: 20px;
}
.tree-details {
padding-bottom: 40px;
}
.tree-details h3 {
font-size: 18px;
position: relative;
}
.toggle-icon {
display: inline-block;
}
.closed .toggle-icon, .closed.toggle-icon {
}
.tree .tree-inner {
cursor: pointer;
padding: 5px 7px 5px 22px;
position: relative;
overflow: hidden;
text-overflow: ellipsis;
}
.tree .toggle-button {
width: 16px;
height: 16px;
margin-left: -18px;
display: inline-grid;
place-content: center;
background: none;
border: none;
transition: transform 200ms;
}
.tree .toggle-button.closed svg {
transform: rotate(-90deg);
}
.tree .toggle-button svg {
transform: rotate(0deg);
width: 16px;
height: 16px;
}
.tree .toggle-icon.empty {
width: 5px;
height: 5px;
position: absolute;
top: 50%;
margin-top: -2px;
margin-left: -13px;
}
.tree .tree-inner {
border-radius: 4px;
}
.tree ul ul .tree-inner {
padding-left: 32px;
}
.tree ul ul ul .tree-inner {
padding-left: 48px;
}
.tree ul ul ul ul .tree-inner {
padding-left: 64px;
}
.tree ul ul ul ul ul .tree-inner {
padding-left: 72px;
}
.tree .tree-inner:hover {
background: var(--gray-200);
}
.tree .tree-inner.active, .tree .tree-inner.active:hover {
background: var(--tree-active-background);
font-weight: bold;
}
.tree-details .toggle-icon {
width: 16px;
height: 16px;
/* vertically center the button */
position: absolute;
top: 50%;
margin-top: -9px;
margin-left: 6px;
}
.badge-error {
float: right;
background: var(--background-error);
color: #FFF;
padding: 1px 4px;
font-size: 10px;
font-weight: bold;
vertical-align: middle;
}
.has-error {
color: var(--color-error);
}
.errors h3 {
color: var(--color-error);
}
.errors th {
background: var(--background-error);
color: #FFF;
}
.errors .toggle-icon {
background-color: var(--background-error);
}
h3 a, h3 a:hover, h3 a:focus {
color: inherit;
text-decoration: inherit;
}
</style>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script>
window.addEventListener('DOMContentLoaded', () => {
new SymfonyProfilerFormPanel();
});
class SymfonyProfilerFormPanel {
#activeTreeItem;
#activeTreePanel;
#storage;
#storageKey = 'sf_toggle_data';
#togglerStates = {};
constructor() {
this.#storage = sessionStorage;
this.#initTrees();
this.#initTogglerButtons();
}
#initTrees() {
const treeItems = document.querySelectorAll('.tree .tree-inner');
treeItems.forEach((treeItem) => {
var targetId = treeItem.getAttribute('data-tab-target-id');
const target = document.getElementById(targetId);
if (!target) {
throw `Tab target ${targetId} does not exist`;
}
treeItem.addEventListener('click', (e) => {
this.#selectTreeItem(treeItem);
e.stopPropagation();
return false;
});
target.classList.add('hidden');
});
if (treeItems.length > 0) {
this.#selectTreeItem(treeItems[0]);
}
};
#selectTreeItem(treeItem) {
const treePanelId = treeItem.getAttribute('data-tab-target-id');
const treePanel = document.getElementById(treePanelId);
if (!treePanel) {
throw `The tree panel ${treePanelId} does not exist`;
}
if (this.#activeTreeItem) {
this.#activeTreeItem.classList.remove('active');
}
if (this.#activeTreePanel) {
this.#activeTreePanel.classList.add('hidden');
}
treeItem.classList.add('active');
treePanel.classList.remove('hidden');
this.#activeTreeItem = treeItem;
this.#activeTreePanel = treePanel;
}
#initTogglerButtons() {
this.#togglerStates = this.#getTogglerStates();
if (!this.#isObject(this.#togglerStates)) {
this.#togglerStates = {};
}
const buttons = document.querySelectorAll('.toggle-button');
buttons.forEach((button) => {
const targetId = button.getAttribute('data-toggle-target-id');
const target = document.getElementById(targetId);
if (!target) {
throw `Toggle target ${targetId} does not exist`;
}
// correct the initial state of the button
if (target.classList.contains('hidden')) {
button.classList.add('closed');
}
button.addEventListener('click', (e) => {
this.#toggleToggler(button);
e.stopPropagation();
return false;
});
if (this.#togglerStates.hasOwnProperty(targetId)) {
// open or collapse based on stored data
if (0 === this.#togglerStates[targetId]) {
this.#collapseToggler(button);
} else {
this.#expandToggler(button);
}
}
});
};
#isTogglerCollapsed(button) {
return button.classList.contains('closed');
}
#isTogglerExpanded(button) {
return !this.#isTogglerCollapsed(button);
}
#expandToggler(button) {
const targetId = button.getAttribute('data-toggle-target-id');
const target = document.getElementById(targetId);
if (!target) {
throw "Toggle target " + targetId + " does not exist";
}
if (this.#isTogglerCollapsed(button)) {
button.classList.remove('closed');
target.classList.remove('hidden');
this.#togglerStates[targetId] = 1;
this.#saveTogglerStates();
}
}
#collapseToggler(button) {
const targetId = button.getAttribute('data-toggle-target-id');
const target = document.getElementById(targetId);
if (!target) {
throw `Toggle target ${targetId} does not exist`;
}
if (this.#isTogglerExpanded(button)) {
button.classList.add('closed');
target.classList.add('hidden');
this.#togglerStates[targetId] = 0;
this.#saveTogglerStates();
}
}
#toggleToggler(button) {
if (button.classList.contains('closed')) {
this.#expandToggler(button);
} else {
this.#collapseToggler(button);
}
}
#saveTogglerStates() {
this.#storage.setItem(this.#storageKey, JSON.stringify(this.#togglerStates));
}
#getTogglerStates() {
const data = this.#storage.getItem(this.#storageKey);
if (null !== data) {
try {
return JSON.parse(data);
} catch(e) {
}
}
return {};
}
#isObject(variable) {
return null !== variable && 'object' === typeof variable && !Array.isArray(variable);
}
}
</script>
{% endblock %}
{% block panel %}
<h2>Forms</h2>
{% if collector.data.forms|length %}
<div id="tree-menu" class="tree">
<ul>
{% for formName, formData in collector.data.forms %}
{{ _self.form_tree_entry(formName, formData, true) }}
{% endfor %}
</ul>
</div>
<div id="tree-details-container">
{% for formName, formData in collector.data.forms %}
{{ _self.form_tree_details(formName, formData, collector.data.forms_by_hash, loop.first) }}
{% endfor %}
</div>
{% else %}
<div class="empty empty-panel">
<p>No forms were submitted.</p>
</div>
{% endif %}
{% endblock %}
{% macro form_tree_entry(name, data, is_root) %}
{% set has_error = data.errors is defined and data.errors|length > 0 %}
<li>
<div class="tree-inner" data-tab-target-id="{{ data.id }}-details" title="{{ name|default('(no name)') }}">
{% if has_error %}
<div class="badge-error">{{ data.errors|length }}</div>
{% endif %}
{% if data.children is not empty %}
<button class="toggle-button" data-toggle-target-id="{{ data.id }}-children">
{{ source('@WebProfiler/Icon/chevron-down.svg') }}
</button>
{% else %}
<div class="toggle-icon empty"></div>
{% endif %}
<span {% if has_error or data.has_children_error|default(false) %}class="has-error"{% endif %}>
{{ name|default('(no name)') }}
</span>
</div>
{% if data.children is not empty %}
<ul id="{{ data.id }}-children" {% if not is_root and not data.has_children_error|default(false) %}class="hidden"{% endif %}>
{% for childName, childData in data.children %}
{{ _self.form_tree_entry(childName, childData, false) }}
{% endfor %}
</ul>
{% endif %}
</li>
{% endmacro %}
{% macro form_tree_details(name, data, forms_by_hash, show) %}
<div class="tree-details{% if not show|default(false) %} hidden{% endif %}" {% if data.id is defined %}id="{{ data.id }}-details"{% endif %}>
<h2>{{ name|default('(no name)') }}</h2>
{% if data.type_class is defined %}
<div class="form-type-class">
<span class="form-type-class-label">Form type:</span>
{{ profiler_dump(data.type_class) }}
</div>
{% endif %}
{% set form_has_errors = data.errors ?? [] is not empty %}
<div class="sf-tabs">
<div class="tab {{ form_has_errors ? 'active' : 'disabled' }}">
<h3 class="tab-title">Errors</h3>
<div class="tab-content">
{{ _self.render_form_errors(data) }}
</div>
</div>
<div class="tab {{ not form_has_errors ? 'active' }}">
<h3 class="tab-title">Default Data</h3>
<div class="tab-content">
{{ _self.render_form_default_data(data) }}
</div>
</div>
<div class="tab {{ (data.submitted_data ?? []) is empty ? 'disabled' }}">
<h3 class="tab-title">Submitted Data</h3>
<div class="tab-content">
{{ _self.render_form_submitted_data(data) }}
</div>
</div>
<div class="tab {{ (data.passed_options ?? []) is empty ? 'disabled' }}">
<h3 class="tab-title">Passed Options</h3>
<div class="tab-content">
{{ _self.render_form_passed_options(data) }}
</div>
</div>
<div class="tab {{ (data.resolved_options ?? []) is empty ? 'disabled' }}">
<h3 class="tab-title">Resolved Options</h3>
<div class="tab-content">
{{ _self.render_form_resolved_options(data) }}
</div>
</div>
<div class="tab {{ (data.view_vars ?? []) is empty ? 'disabled' }}">
<h3 class="tab-title">View Vars</h3>
<div class="tab-content">
{{ _self.render_form_view_variables(data) }}
</div>
</div>
</div>
</div>
{% for childName, childData in data.children %}
{{ _self.form_tree_details(childName, childData, forms_by_hash) }}
{% endfor %}
{% endmacro %}
{% macro render_form_errors(data) %}
{% if data.errors is defined and data.errors|length > 0 %}
<div class="errors">
<table id="{{ data.id }}-errors">
<thead>
<tr>
<th>Message</th>
<th>Origin</th>
<th>Cause</th>
</tr>
</thead>
<tbody>
{% for error in data.errors %}
<tr>
<td>{{ error.message }}</td>
<td>
{% if error.origin is empty %}
<em>This form.</em>
{% elseif forms_by_hash[error.origin] is not defined %}
<em>Unknown.</em>
{% else %}
{{ forms_by_hash[error.origin].name }}
{% endif %}
</td>
<td>
{% if error.trace %}
<span class="newline">Caused by:</span>
{% for stacked in error.trace %}
{{ profiler_dump(stacked) }}
{% endfor %}
{% else %}
<em>Unknown.</em>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty">
<p>This form has no errors.</p>
</div>
{% endif %}
{% endmacro %}
{% macro render_form_default_data(data) %}
{% if data.default_data is defined %}
<table>
<thead>
<tr>
<th style="width: 180px">Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<th class="font-normal" scope="row">Model Format</th>
<td>
{% if data.default_data.model is defined %}
{{ profiler_dump(data.default_data.seek('model')) }}
{% else %}
<em class="font-normal text-muted">same as normalized format</em>
{% endif %}
</td>
</tr>
<tr>
<th class="font-normal" scope="row">Normalized Format</th>
<td>{{ profiler_dump(data.default_data.seek('norm')) }}</td>
</tr>
<tr>
<th class="font-normal" scope="row">View Format</th>
<td>
{% if data.default_data.view is defined %}
{{ profiler_dump(data.default_data.seek('view')) }}
{% else %}
<em class="font-normal text-muted">same as normalized format</em>
{% endif %}
</td>
</tr>
</tbody>
</table>
{% else %}
<div class="empty">
<p>This form has default data defined.</p>
</div>
{% endif %}
{% endmacro %}
{% macro render_form_submitted_data(data) %}
{% if data.submitted_data.norm is defined %}
<table>
<thead>
<tr>
<th style="width: 180px">Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<th class="font-normal" scope="row">View Format</th>
<td>
{% if data.submitted_data.view is defined %}
{{ profiler_dump(data.submitted_data.seek('view')) }}
{% else %}
<em class="font-normal text-muted">same as normalized format</em>
{% endif %}
</td>
</tr>
<tr>
<th class="font-normal" scope="row">Normalized Format</th>
<td>{{ profiler_dump(data.submitted_data.seek('norm')) }}</td>
</tr>
<tr>
<th class="font-normal" scope="row">Model Format</th>
<td>
{% if data.submitted_data.model is defined %}
{{ profiler_dump(data.submitted_data.seek('model')) }}
{% else %}
<em class="font-normal text-muted">same as normalized format</em>
{% endif %}
</td>
</tr>
</tbody>
</table>
{% else %}
<div class="empty">
<p>This form was not submitted.</p>
</div>
{% endif %}
{% endmacro %}
{% macro render_form_passed_options(data) %}
{% if data.passed_options ?? [] is not empty %}
<table>
<thead>
<tr>
<th style="width: 180px">Option</th>
<th>Passed Value</th>
<th>Resolved Value</th>
</tr>
</thead>
<tbody>
{% for option, value in data.passed_options %}
<tr>
<th>{{ option }}</th>
<td>{{ profiler_dump(value) }}</td>
<td>
{# values can be stubs #}
{% set option_value = (value.value is defined) ? value.value : value %}
{% set resolved_option_value = (data.resolved_options[option].value is defined)
? data.resolved_options[option].value
: data.resolved_options[option] %}
{% if resolved_option_value == option_value %}
<em class="font-normal text-muted">same as passed value</em>
{% else %}
{{ profiler_dump(data.resolved_options.seek(option)) }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty">
<p>No options were passed when constructing this form.</p>
</div>
{% endif %}
{% endmacro %}
{% macro render_form_resolved_options(data) %}
<table>
<thead>
<tr>
<th style="width: 180px">Option</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for option, value in data.resolved_options ?? [] %}
<tr>
<th scope="row">{{ option }}</th>
<td>{{ profiler_dump(value) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endmacro %}
{% macro render_form_view_variables(data) %}
<table>
<thead>
<tr>
<th style="width: 180px">Variable</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for variable, value in data.view_vars ?? [] %}
<tr>
<th scope="row">{{ variable }}</th>
<td>{{ profiler_dump(value) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endmacro %}

View File

@@ -0,0 +1,175 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block head %}
{{ parent() }}
<style>
.sf-profiler-httpclient-requests thead th {
vertical-align: top;
}
.sf-profiler-httpclient-requests .http-method {
border: 1px solid var(--header-status-request-method-color);
border-radius: 5px;
color: var(--header-status-request-method-color);
display: inline-block;
font-weight: 500;
line-height: 1;
margin-right: 6px;
padding: 2px 4px;
text-align: center;
white-space: nowrap;
}
.sf-profiler-httpclient-requests .status-response-status-code {
background: var(--gray-600);
border-radius: 4px;
color: var(--white);
display: inline-block;
font-size: 12px;
font-weight: bold;
margin-bottom: 2px;
padding: 1px 3px;
}
.sf-profiler-httpclient-requests .status-response-status-code.status-success { background: var(--header-success-status-code-background); color: var(--header-success-status-code-color); }
.sf-profiler-httpclient-requests .status-response-status-code.status-warning { background: var(--header-warning-status-code-background); color: var(--header-warning-status-code-color); }
.sf-profiler-httpclient-requests .status-response-status-code.status-error { background: var(--header-error-status-code-background); color: var(--header-error-status-code-color); }
</style>
{% endblock %}
{% block toolbar %}
{% if collector.requestCount %}
{% set icon %}
{{ source('@WebProfiler/Icon/http-client.svg') }}
{% set status_color = '' %}
<span class="sf-toolbar-value">{{ collector.requestCount }}</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Total requests</b>
<span>{{ collector.requestCount }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>HTTP errors</b>
<span class="sf-toolbar-status {{ collector.errorCount > 0 ? 'sf-toolbar-status-red' }}">{{ collector.errorCount }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label {{ collector.requestCount == 0 ? 'disabled' }}">
<span class="icon">{{ source('@WebProfiler/Icon/http-client.svg') }}</span>
<strong>HTTP Client</strong>
{% if collector.requestCount %}
<span class="count">
{{ collector.requestCount }}
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
<h2>HTTP Client</h2>
{% if collector.requestCount == 0 %}
<div class="empty empty-panel">
<p>No HTTP requests were made.</p>
</div>
{% else %}
<div class="metrics">
<div class="metric">
<span class="value">{{ collector.requestCount }}</span>
<span class="label">Total requests</span>
</div>
<div class="metric">
<span class="value">{{ collector.errorCount }}</span>
<span class="label">HTTP errors</span>
</div>
</div>
<h2>Clients</h2>
<div class="sf-tabs">
{% for name, client in collector.clients %}
<div class="tab {{ client.traces|length == 0 ? 'disabled' }}">
<h3 class="tab-title">{{ name }} <span class="badge">{{ client.traces|length }}</span></h3>
<div class="tab-content">
{% if client.traces|length == 0 %}
<div class="empty">
<p>No requests were made with the "{{ name }}" service.</p>
</div>
{% else %}
<h4>Requests</h4>
{% for trace in client.traces %}
{% set profiler_token = '' %}
{% set profiler_link = '' %}
{% if trace.info.response_headers is defined %}
{% for header in trace.info.response_headers %}
{% if header matches '/^x-debug-token: .*$/i' %}
{% set profiler_token = (header.getValue | slice('x-debug-token: ' | length)) %}
{% endif %}
{% if header matches '/^x-debug-token-link: .*$/i' %}
{% set profiler_link = (header.getValue | slice('x-debug-token-link: ' | length)) %}
{% endif %}
{% endfor %}
{% endif %}
<table class="sf-profiler-httpclient-requests">
<thead>
<tr>
<th>
<span class="http-method">{{ trace.method }}</span>
</th>
<th class="full-width">
{{ trace.url }}
</th>
{% if profiler_token and profiler_link %}
<th>
Profile
</th>
{% endif %}
{% if trace.curlCommand is defined and trace.curlCommand %}
<th>
<button class="btn btn-sm hidden" title="Copy as cURL" data-clipboard-text="{{ trace.curlCommand }}">Copy as cURL</button>
</th>
{% endif %}
</tr>
</thead>
<tbody>
{% if trace.options is not empty %}
<tr>
<th class="font-normal">Request options</th>
<td>{{ profiler_dump(trace.options, maxDepth=1) }}</td>
</tr>
{% endif %}
<tr>
<th class="font-normal">Response</th>
<td{% if trace.curlCommand is defined and trace.curlCommand %} colspan="2"{% endif %}>
{% if trace.http_code >= 500 %}
{% set responseStatus = 'error' %}
{% elseif trace.http_code >= 400 %}
{% set responseStatus = 'warning' %}
{% else %}
{% set responseStatus = 'success' %}
{% endif %}
<span class="font-normal status-response-status-code status-{{ responseStatus }}">
{{ trace.http_code }}
</span>
{{ profiler_dump(trace.info, maxDepth=1) }}
</td>
{% if profiler_token and profiler_link %}
<td>
<span><a href="{{ profiler_link }}" target="_blank">{{ profiler_token }}</a></span>
</td>
{% endif %}
</tr>
</tbody>
</table>
{% endfor %}
{% endif %}
</div>
</div>
{% endfor %}
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,581 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block stylesheets %}
{{ parent() }}
<style>
:root {
--log-filter-active-num-color: #2563EB;
--log-timestamp-color: #555;
}
.theme-dark {
--log-filter-active-num-color: #2563EB;
--log-timestamp-color: #ccc;
}
.log-filters {
display: flex;
flex-wrap: wrap;
}
.log-filters .log-filter {
flex-shrink: 0;
margin-right: 15px;
position: relative;
}
.log-filters .log-filter summary {
align-items: center;
background: var(--button-background);
border-radius: 6px;
border: 1px solid var(--button-border-color);
box-shadow: var(--button-box-shadow);
color: var(--button-color);
cursor: pointer;
display: flex;
font-size: 13px;
font-weight: 500;
padding: 4px 8px;
white-space: nowrap;
}
.log-filters .log-filter summary:active {
box-shadow: none;
transform: translateY(1px);
}
.log-filters .log-filter summary .icon {
height: 18px;
width: 18px;
margin: 0 7px 0 0;
}
.log-filters .log-filter summary svg {
height: 18px;
width: 18px;
opacity: 0.7;
}
.log-filters .log-filter summary svg {
stroke-width: 2;
}
.log-filters .log-filter summary .filter-active-num {
color: var(--log-filter-active-num-color);
font-weight: bold;
padding: 0 1px;
}
.log-filter .tab-navigation {
position: relative;
}
.log-filter .tab-navigation input[type="radio"] {
position: absolute;
pointer-events: none;
opacity: 0;
}
.tab-navigation input[type="radio"]:checked + .tab-control {
background-color: var(--tab-active-background);
border-radius: 6px;
box-shadow: inset 0 0 0 1.5px var(--tab-active-border-color);
color: var(--tab-active-color);
position: relative;
z-index: 1;
}
.theme-dark .tab-navigation input[type="radio"]:checked + .tab-control {
box-shadow: inset 0 0 0 1px var(--tab-border-color);
}
.tab-navigation input[type="radio"]:focus-visible + .tab-control {
outline: 1px solid var(--color-link);
}
.tab-navigation input[type="radio"]:checked + .tab-control + input[type="radio"] + .tab-control:before{
width: 0;
}
.log-filters .log-filter .log-filter-content {
background: var(--base-0);
border: 1px solid var(--table-border-color);
border-radius: 6px;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
padding: 15px;
position: absolute;
left: 0;
top: 32px;
max-width: 400px;
min-width: 200px;
z-index: 9999;
}
.log-filters .log-filter .log-filter-content .log-filter-option {
align-items: center;
display: flex;
}
.log-filter .filter-select-all-or-none {
margin-bottom: 10px;
}
.log-filter .filter-select-all-or-none button + button {
margin-left: 15px;
}
.log-filters .log-filter .log-filter-content .log-filter-option + .log-filter-option {
margin: 7px 0 0;
}
.log-filters .log-filter .log-filter-content .log-filter-option label {
cursor: pointer;
flex: 1;
padding-left: 10px;
}
table.logs {
border-bottom-width: 0;
border-collapse: collapse;
}
table.logs tr + tr td {
border-width: 1px 0 0;
}
table.logs .metadata {
display: block;
font-size: 12px;
}
.theme-dark tr.status-error td,
.theme-dark tr.status-warning td { border-bottom: unset; border-top: unset; }
table.logs .log-timestamp {
color: var(--log-timestamp-color);
}
table.logs .log-metadata {
margin: 8px 0 0;
}
table.logs .log-metadata > span {
display: inline-block;
}
table.logs .log-metadata > span + span {
margin-left: 10px;
}
table.logs .log-metadata .log-channel {
color: var(--base-1);
font-size: 13px;
font-weight: bold;
}
table.logs .log-metadata .badge {
background: var(--badge-light-background);
color: var(--badge-light-color);
}
table.logs .log-metadata .log-num-occurrences {
color: var(--color-muted);
font-size: 13px;
}
table.logs .log-metadata .context {
background: var(--code-block-background);
border-radius: 4px;
padding: 5px;
}
table.logs .log-metadata .context + .context {
margin-top: 10px;
}
.log-type-badge {
background: var(--badge-light-background);
box-shadow: none;
color: var(--badge-light-color);
display: inline-block;
font-family: var(--font-family-system);
margin-top: 5px;
}
.log-type-badge.badge-deprecation,
.log-type-badge.badge-warning {
background: var(--badge-warning-background);
color: var(--badge-warning-color);
}
.log-type-badge.badge-error {
background: var(--badge-danger-background);
color: var(--badge-danger-color);
}
.log-type-badge.badge-silenced {
background: #EDE9FE;
color: #6D28D9;
}
.theme-dark .log-type-badge.badge-silenced {
background: #5B21B6;
color: #EDE9FE;
}
tr.log-status-warning > td:first-child,
tr.log-status-error > td:first-child,
tr.log-status-silenced > td:first-child {
position: relative;
}
tr.log-status-warning > td:first-child:before,
tr.log-status-error > td:first-child:before,
tr.log-status-silenced > td:first-child:before {
background: transparent;
border-radius: 0;
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
}
tr.log-status-warning > td:first-child:before {
background: var(--yellow-400);
}
tr.log-status-error > td:first-child:before {
background: var(--red-400);
}
tr.log-status-silenced > td:first-child:before {
background: #a78bfa;
}
.container-compilation-logs {
background: var(--table-background);
border: 1px solid var(--base-2);
border-radius: 6px;
margin-top: 30px;
padding: 15px;
}
.container-compilation-logs summary {
cursor: pointer;
}
.container-compilation-logs summary h4 {
margin: 0 0 5px;
}
.container-compilation-logs summary p {
margin: 0;
}
</style>
{% endblock %}
{% block javascripts %}
<script>
window.addEventListener('DOMContentLoaded', () => {
new SymfonyProfilerLoggerPanel();
});
class SymfonyProfilerLoggerPanel {
constructor() {
this.#initializeLogsTable();
}
#initializeLogsTable() {
this.#updateLogsTable();
document.querySelectorAll('.log-filter input').forEach((input) => {
input.addEventListener('change', () => { this.#updateLogsTable(); });
});
document.querySelectorAll('.filter-select-all-or-none button').forEach((link) => {
link.addEventListener('click', () => {
const selectAll = link.classList.contains('select-all');
link.closest('.log-filter-content').querySelectorAll('input').forEach((input) => {
input.checked = selectAll;
});
this.#updateLogsTable();
});
});
document.body.addEventListener('click', (event) => {
document.querySelectorAll('details.log-filter').forEach((filterElement) => {
if (!filterElement.contains(event.target) && filterElement.open) {
filterElement.open = false;
}
});
});
}
#updateLogsTable() {
const logs = document.querySelector('table.logs');
if (null === logs) {
return;
}
const selectedType = document.querySelector('#log-filter-type input:checked').value;
const priorities = document.querySelectorAll('#log-filter-priority input');
const allPriorities = Array.from(priorities).map((input) => input.value);
const selectedPriorities = Array.from(priorities).filter((input) => input.checked).map((input) => input.value);
const channels = document.querySelectorAll('#log-filter-channel input');
const selectedChannels = Array.from(channels).filter((input) => input.checked).map((input) => input.value);
/* hide rows that don't match the current filters */
let numVisibleRows = 0;
logs.querySelectorAll('tbody tr').forEach((row) => {
if ('all' !== selectedType && selectedType !== row.getAttribute('data-type')) {
row.style.display = 'none';
return;
}
const priority = row.getAttribute('data-priority');
if (false === selectedPriorities.includes(priority) && true === allPriorities.includes(priority)) {
row.style.display = 'none';
return;
}
if ('' !== row.getAttribute('data-channel') && false === selectedChannels.includes(row.getAttribute('data-channel'))) {
row.style.display = 'none';
return;
}
row.style.display = 'table-row';
numVisibleRows++;
});
document.querySelector('table.logs').style.display = 0 === numVisibleRows ? 'none' : 'table';
document.querySelector('.no-logs-message').style.display = 0 === numVisibleRows ? 'block' : 'none';
/* update the selected totals of all filters */
document.querySelector('#log-filter-priority .filter-active-num').innerText = (priorities.length === selectedPriorities.length) ? 'All' : selectedPriorities.length;
document.querySelector('#log-filter-channel .filter-active-num').innerText = (channels.length === selectedChannels.length) ? 'All' : selectedChannels.length;
}
}
</script>
{% endblock %}
{% block toolbar %}
{% if collector.counterrors or collector.countdeprecations or collector.countwarnings %}
{% set icon %}
{% set status_color = collector.counterrors ? 'red' : collector.countwarnings ? 'yellow' : 'none' %}
{{ source('@WebProfiler/Icon/logger.svg') }}
<span class="sf-toolbar-value">{{ collector.counterrors ?: (collector.countdeprecations + collector.countwarnings) }}</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Errors</b>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.counterrors ? 'red' }}">{{ collector.counterrors|default(0) }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Warnings</b>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.countwarnings ? 'yellow' }}">{{ collector.countwarnings|default(0) }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Deprecations</b>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.countdeprecations ? 'none' }}">{{ collector.countdeprecations|default(0) }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label label-status-{{ collector.counterrors ? 'error' : collector.countwarnings ? 'warning' : 'none' }} {{ collector.logs is empty ? 'disabled' }}">
<span class="icon">{{ source('@WebProfiler/Icon/logger.svg') }}</span>
<strong>Logs</strong>
{% if collector.counterrors or collector.countdeprecations or collector.countwarnings %}
<span class="count">
<span>{{ collector.counterrors ?: (collector.countdeprecations + collector.countwarnings) }}</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
<h2>Log Messages</h2>
{% if collector.processedLogs is empty %}
<div class="empty empty-panel">
<p>No log messages available.</p>
</div>
{% else %}
{% set has_error_logs = collector.processedLogs|column('type')|filter(type => 'error' == type)|length > 0 %}
{% set has_deprecation_logs = collector.processedLogs|column('type')|filter(type => 'deprecation' == type)|length > 0 %}
{% set filters = collector.filters %}
<div class="log-filters">
<div id="log-filter-type" class="log-filter">
<div class="tab-navigation">
{% set initially_active_tab = has_error_logs ? 'error' : has_deprecation_logs ? 'deprecation' : 'all' %}
<input type="radio" id="filter-log-type-all" name="filter-log-type" value="all" {{ 'all' == initially_active_tab ? 'checked' }}>
<label for="filter-log-type-all" class="tab-control">
All messages
</label>
<input type="radio" id="filter-log-type-error" name="filter-log-type" value="error" {{ 'error' == initially_active_tab ? 'checked' }}>
<label for="filter-log-type-error" class="tab-control">
Errors
<span class="badge status-{{ collector.counterrors ? 'error' }}">{{ collector.counterrors|default(0) }}</span>
</label>
<input type="radio" id="filter-log-type-deprecation" name="filter-log-type" value="deprecation" {{ 'deprecation' == initially_active_tab ? 'checked' }}>
<label for="filter-log-type-deprecation" class="tab-control">
Deprecations
<span class="badge status-{{ collector.countdeprecations ? 'warning' }}">{{ collector.countdeprecations|default(0) }}</span>
</label>
</div>
</div>
<details id="log-filter-priority" class="log-filter">
<summary>
<span class="icon">{{ source('@WebProfiler/Icon/filter.svg') }}</span>
Level (<span class="filter-active-num">{{ filters.priority|length - 1 }}</span>)
</summary>
<div class="log-filter-content">
<div class="filter-select-all-or-none">
<button type="button" class="btn btn-link select-all">Select All</button>
<button type="button" class="btn btn-link select-none">Select None</button>
</div>
{% for label, value in filters.priority %}
<div class="log-filter-option">
<input {{ 'debug' != value ? 'checked' }} type="checkbox" id="filter-log-level-{{ value }}" name="filter-log-level-{{ value }}" value="{{ value }}">
<label for="filter-log-level-{{ value }}">{{ label }}</label>
</div>
{% endfor %}
</div>
</details>
<details id="log-filter-channel" class="log-filter">
<summary>
<span class="icon">{{ source('@WebProfiler/Icon/filter.svg') }}</span>
Channel (<span class="filter-active-num">{{ filters.channel|length - 1 }}</span>)
</summary>
<div class="log-filter-content">
<div class="filter-select-all-or-none">
<button type="button" class="btn btn-link select-all">Select All</button>
<button type="button" class="btn btn-link select-none">Select None</button>
</div>
{% for value in filters.channel %}
<div class="log-filter-option">
<input {{ 'event' != value ? 'checked' }} type="checkbox" id="filter-log-channel-{{ value }}" name="filter-log-channel-{{ value }}" value="{{ value }}">
<label for="filter-log-channel-{{ value }}">{{ value|title }}</label>
</div>
{% endfor %}
</div>
</details>
</div>
<table class="logs">
<colgroup>
<col style="width: 140px">
<col>
</colgroup>
<thead>
<tr>
<th>Time</th>
<th>Message</th>
</tr>
</thead>
<tbody>
{% for log in collector.processedLogs %}
{% set css_class = 'error' == log.type ? 'error'
: (log.priorityName == 'WARNING' or 'deprecation' == log.type) ? 'warning'
: 'silenced' == log.type ? 'silenced'
%}
<tr class="log-status-{{ css_class }}" data-type="{{ log.type }}" data-priority="{{ log.priority }}" data-channel="{{ log.channel }}" style="{{ 'event' == log.channel or 'DEBUG' == log.priorityName ? 'display: none' }}">
<td class="log-timestamp">
<time class="newline" title="{{ log.timestamp|date('r') }}" datetime="{{ log.timestamp|date(constant('DateTimeInterface::RFC3339_EXTENDED')) }}" data-convert-to-user-timezone data-render-as-time data-render-with-millisecond-precision>
{{ log.timestamp|date('H:i:s.v') }}
</time>
{% if log.type in ['error', 'deprecation', 'silenced'] or 'WARNING' == log.priorityName %}
<span class="log-type-badge badge badge-{{ css_class }}">
{% if 'error' == log.type or 'WARNING' == log.priorityName %}
{{ log.priorityName|lower }}
{% else %}
{{ log.type|lower }}
{% endif %}
</span>
{% else %}
<span class="log-type-badge badge badge-{{ css_class }}">
{{ log.priorityName|lower }}
</span>
{% endif %}
</td>
<td class="font-normal">
{{ _self.render_log_message('debug', loop.index, log) }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="no-logs-message empty">
<p>There are no log messages.</p>
</div>
{% endif %}
{% set compilerLogTotal = collector.compilerLogs|reduce((total, logs) => total + logs|length, 0) %}
<details class="container-compilation-logs">
<summary>
<h4>Container Compilation Logs <span class="text-muted">({{ compilerLogTotal }})</span></h4>
<span class="text-muted">Log messages generated during the compilation of the service container.</span>
</summary>
{% if collector.compilerLogs is empty %}
<div class="empty">
<p>There are no compiler log messages.</p>
</div>
{% else %}
<table class="container-logs">
<thead>
<tr>
<th>Messages</th>
<th class="full-width">Class</th>
</tr>
</thead>
<tbody>
{% for class, logs in collector.compilerLogs %}
<tr>
<td class="font-normal text-right">{{ logs|length }}</td>
<td class="font-normal">
{% set context_id = 'context-compiler-' ~ loop.index %}
<button type="button" class="btn btn-link sf-toggle" data-toggle-selector="#{{ context_id }}" data-toggle-alt-content="{{ class }}">{{ class }}</button>
<div id="{{ context_id }}" class="context sf-toggle-content sf-toggle-hidden">
<ul class="break-long-words">
{% for log in logs %}
<li>{{ profiler_dump_log(log.message) }}</li>
{% endfor %}
</ul>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</details>
{% endblock %}
{% macro render_log_message(category, log_index, log) %}
{% set has_context = log.context is defined and log.context is not empty %}
{% set has_trace = log.context.exception.trace is defined %}
{% if not has_context %}
{{ profiler_dump_log(log.message) }}
{% else %}
{{ profiler_dump_log(log.message, log.context) }}
{% endif %}
<div class="log-metadata">
{% if log.channel %}
<span class="badge">{{ log.channel }}</span>
{% endif %}
{% if log.errorCount is defined and log.errorCount > 1 %}
<span class="log-num-occurrences">{{ log.errorCount }} times</span>
{% endif %}
{% if has_context %}
{% set context_id = 'context-' ~ category ~ '-' ~ log_index %}
<span><button type="button" class="btn btn-link text-small sf-toggle" data-toggle-selector="#{{ context_id }}" data-toggle-alt-content="Hide context">Show context</button></span>
{% endif %}
{% if has_trace %}
{% set trace_id = 'trace-' ~ category ~ '-' ~ log_index %}
<span><button type="button" class="btn btn-link text-small sf-toggle" data-toggle-selector="#{{ trace_id }}" data-toggle-alt-content="Hide trace">Show trace</button></span>
{% endif %}
{% if has_context %}
<div id="{{ context_id }}" class="context sf-toggle-content sf-toggle-hidden">
{{ profiler_dump(log.context, maxDepth=1) }}
</div>
{% endif %}
{% if has_trace %}
<div id="{{ trace_id }}" class="context sf-toggle-content sf-toggle-hidden">
{{ profiler_dump(log.context.exception.trace, maxDepth=1) }}
</div>
{% endif %}
</div>
{% endmacro %}

View File

@@ -0,0 +1,511 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block stylesheets %}
{{ parent() }}
<style>
:root {
--mailer-email-table-wrapper-background: var(--gray-100);
--mailer-email-table-active-row-background: #dbeafe;
--mailer-email-table-active-row-color: var(--color-text);
}
.theme-dark {
--mailer-email-table-wrapper-background: var(--gray-900);
--mailer-email-table-active-row-background: var(--gray-300);
--mailer-email-table-active-row-color: var(--gray-800);
}
.mailer-email-summary-table-wrapper {
background: var(--mailer-email-table-wrapper-background);
border-bottom: 4px double var(--table-border-color);
border-radius: inherit;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
margin: 0 -9px 10px -9px;
padding-bottom: 10px;
transform: translateY(-9px);
max-height: 265px;
overflow-y: auto;
}
.mailer-email-summary-table,
.mailer-email-summary-table tr,
.mailer-email-summary-table td {
border: 0;
border-radius: inherit;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
box-shadow: none;
}
.mailer-email-summary-table th {
color: var(--color-muted);
font-size: 13px;
padding: 4px 10px;
}
.mailer-email-summary-table tr td,
.mailer-email-summary-table tr:last-of-type td {
border: solid var(--table-border-color);
border-width: 1px 0;
}
.mailer-email-summary-table-row {
margin: 5px 0;
}
.mailer-email-summary-table-row:hover {
cursor: pointer;
}
.mailer-email-summary-table-row.active {
background: var(--mailer-email-table-active-row-background);
color: var(--mailer-email-table-active-row-color);
}
.mailer-email-summary-table-row td {
font-family: var(--font-family-system);
font-size: inherit;
}
.mailer-email-details {
display: none;
}
.mailer-email-details.active {
display: block;
}
.mailer-transport-information {
border-bottom: 1px solid var(--form-input-border-color);
padding-bottom: 5px;
font-size: var(--font-size-body);
margin: 5px 0 10px 5px;
}
.mailer-transport-information .badge {
font-size: inherit;
font-weight: inherit;
}
.mailer-message-subject {
font-size: 21px;
font-weight: bold;
margin: 5px;
}
.mailer-message-headers {
margin-bottom: 10px;
}
.mailer-message-headers p {
font-size: var(--font-size-body);
margin: 2px 5px;
}
.mailer-message-header-secondary {
color: var(--color-muted);
}
.mailer-message-attachments-title {
align-items: center;
display: flex;
font-size: var(--font-size-body);
font-weight: 600;
margin-bottom: 10px;
}
.mailer-message-attachments-title svg {
color: var(--color-muted);
margin-right: 5px;
height: 18px;
width: 18px;
}
.mailer-message-attachments-title span {
font-weight: normal;
margin-left: 4px;
}
.mailer-message-attachments-list {
list-style: none;
margin: 0 0 5px 20px;
padding: 0;
}
.mailer-message-attachments-list li {
align-items: center;
display: flex;
}
.mailer-message-attachments-list li svg {
margin-right: 5px;
height: 18px;
width: 18px;
}
.mailer-message-attachments-list li a {
margin-left: 5px;
}
.mailer-email-body {
margin: 0;
padding: 6px 8px;
}
.mailer-empty-email-body {
background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' stroke='%23e5e5e5' stroke-width='4' stroke-dasharray='6%2c 14' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e");
border-radius: 6px;
color: var(--color-muted);
margin: 1em 0 0;
padding: .5em 1em;
}
.theme-dark .mailer-empty-email-body {
background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' stroke='%23737373' stroke-width='4' stroke-dasharray='6%2c 14' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e");
}
.mailer-empty-email-body p {
font-size: var(--font-size-body);
margin: 0;
padding: 0.5em 0;
}
.mailer-message-download-raw {
align-items: center;
display: flex;
padding: 5px 0 0 5px;
}
.mailer-message-download-raw svg {
height: 18px;
width: 18px;
margin-right: 3px;
}
</style>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script>
window.addEventListener('DOMContentLoaded', () => {
new SymfonyProfilerMailerPanel();
});
class SymfonyProfilerMailerPanel {
constructor() {
this.#initializeEmailsTable();
}
#initializeEmailsTable() {
const emailRows = document.querySelectorAll('.mailer-email-summary-table-row');
emailRows.forEach((emailRow) => {
emailRow.addEventListener('click', () => {
emailRows.forEach((row) => row.classList.remove('active'));
emailRow.classList.add('active');
document.querySelectorAll('.mailer-email-details').forEach((emailDetails) => emailDetails.style.display = 'none');
document.querySelector(emailRow.getAttribute('data-target')).style.display = 'block';
});
});
}
}
</script>
{% endblock %}
{% block toolbar %}
{% set events = collector.events %}
{% if events.messages|length %}
{% set icon %}
{{ source('@WebProfiler/Icon/mailer.svg') }}
<span class="sf-toolbar-value">{{ events.messages|length }}</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Queued messages</b>
<span class="sf-toolbar-status">{{ events.events|filter(e => e.isQueued())|length }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Sent messages</b>
<span class="sf-toolbar-status">{{ events.events|filter(e => not e.isQueued())|length }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { 'link': profiler_url }) }}
{% endif %}
{% endblock %}
{% block menu %}
{% set events = collector.events %}
<span class="label {{ events.messages is empty ? 'disabled' }}">
<span class="icon">{{ source('@WebProfiler/Icon/mailer.svg') }}</span>
<strong>Emails</strong>
{% if events.messages|length > 0 %}
<span class="count">
<span>{{ events.messages|length }}</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
{% set events = collector.events %}
<h2>Emails</h2>
{% if not events.messages|length %}
<div class="empty empty-panel">
<p>No emails were sent.</p>
</div>
{% else %}
<div class="metrics">
<div class="metric-group">
<div class="metric">
<span class="value">{{ events.events|filter(e => e.isQueued())|length }}</span>
<span class="label">Queued</span>
</div>
<div class="metric">
<span class="value">{{ events.events|filter(e => not e.isQueued())|length }}</span>
<span class="label">Sent</span>
</div>
</div>
</div>
{% endif %}
{% if events.transports|length > 1 %}
{% for transport in events.transports %}
<h2><code>{{ transport }}</code> transport</h2>
{{ _self.render_transport_details(collector, transport) }}
{% endfor %}
{% elseif events.transports is not empty %}
{{ _self.render_transport_details(collector, events.transports|first, true) }}
{% endif %}
{% macro render_transport_details(collector, transport, show_transport_name = false) %}
<div class="card">
{% set num_emails = collector.events.events(transport)|length %}
{% if num_emails > 1 %}
<div class="mailer-email-summary-table-wrapper">
<table class="mailer-email-summary-table">
<thead>
<tr>
<th>#</th>
<th>Subject</th>
<th>To</th>
<th class="visually-hidden">Actions</th>
</tr>
</thead>
<tbody>
{% for event in collector.events.events(transport) %}
<tr class="mailer-email-summary-table-row {{ loop.first ? 'active' }}" data-target="#email-{{ loop.index }}">
<td>{{ loop.index }}</td>
<td>
{% if event.message.subject is defined %}
{{ event.message.getSubject() ?? '(No subject)' }}
{% elseif event.message.headers.has('subject') %}
{{ event.message.headers.get('subject').bodyAsString()|default('(No subject)') }}
{% else %}
(No subject)
{% endif %}
</td>
<td>
{% if event.message.to is defined %}
{{ event.message.getTo()|map(addr => addr.toString())|join(', ')|default('(empty)') }}
{% elseif event.message.headers.has('to') %}
{{ event.message.headers.get('to').bodyAsString()|default('(empty)') }}
{% else %}
(empty)
{% endif %}
</td>
<td class="visually-hidden"><button class="mailer-email-summary-table-row-button" data-target="#email-{{ loop.index }}">View email details</button></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% for event in collector.events.events(transport) %}
<div class="mailer-email-details {{ loop.first ? 'active' }}" id="email-{{ loop.index }}">
{{ _self.render_email_details(collector, transport, event.message, event.isQueued, show_transport_name) }}
</div>
{% endfor %}
{% else %}
{% set event = (collector.events.events(transport)|first) %}
{{ _self.render_email_details(collector, transport, event.message, event.isQueued, show_transport_name) }}
{% endif %}
</div>
{% endmacro %}
{% macro render_email_details(collector, transport, message, message_is_queued, show_transport_name = false) %}
{% if show_transport_name %}
<p class="mailer-transport-information">
<strong>Status:</strong> <span class="badge badge-{{ message_is_queued ? 'warning' : 'success' }}">{{ message_is_queued ? 'Queued' : 'Sent' }}</span>
&bull;
<strong>Transport:</strong> <code>{{ transport }}</code>
</p>
{% endif %}
{% if message.headers is not defined %}
{# render the raw message contents #}
<a class="mailer-message-download-raw" href="data:application/octet-stream;base64,{{ collector.base64Encode(message.toString()) }}" download="email.eml">
{{ source('@WebProfiler/Icon/download.svg') }}
Download as EML file
</a>
<pre class="prewrap" style="max-height: 600px; margin-left: 5px">{{ message.toString() }}</pre>
{% else %}
<div class="sf-tabs">
<div class="tab">
<h3 class="tab-title">Email contents</h3>
<div class="tab-content">
<div class="card-block">
<p class="mailer-message-subject">
{% if message.subject is defined %}
{{ message.getSubject() ?? '(No subject)' }}
{% elseif message.headers.has('subject') %}
{{ message.headers.get('subject').bodyAsString()|default('(No subject)') }}
{% else %}
(No subject)
{% endif %}
</p>
<div class="mailer-message-headers">
<p>
<strong>From:</strong>
{% if message.from is defined %}
{{ message.getFrom()|map(addr => addr.toString())|join(', ')|default('(empty)') }}
{% elseif message.headers.has('from') %}
{{ message.headers.get('from').bodyAsString()|default('(empty)') }}
{% else %}
(empty)
{% endif %}
</p>
<p>
<strong>To:</strong>
{% if message.to is defined %}
{{ message.getTo()|map(addr => addr.toString())|join(', ')|default('(empty)') }}
{% elseif message.headers.has('to') %}
{{ message.headers.get('to').bodyAsString()|default('(empty)') }}
{% else %}
(empty)
{% endif %}
</p>
{% for header in message.headers.all|filter(header => (header.name ?? '')|lower not in ['subject', 'from', 'to']) %}
<p class="mailer-message-header-secondary">{{ header.toString }}</p>
{% endfor %}
</div>
</div>
{% if message.attachments is defined and message.attachments %}
<div class="card-block">
{% set num_of_attachments = message.attachments|length %}
{% set total_attachments_size_in_bytes = message.attachments|reduce((total_size, attachment) => total_size + attachment.body|length, 0) %}
<p class="mailer-message-attachments-title">
{{ source('@WebProfiler/Icon/attachment.svg') }}
Attachments <span>({{ num_of_attachments }} file{{ num_of_attachments > 1 ? 's' }} / {{ _self.render_file_size_humanized(total_attachments_size_in_bytes) }})</span>
</p>
<ul class="mailer-message-attachments-list">
{% for attachment in message.attachments %}
<li>
{{ source('@WebProfiler/Icon/file.svg') }}
{% if attachment.filename|default %}
{{ attachment.filename }}
{% else %}
<em>(no filename)</em>
{% endif %}
({{ _self.render_file_size_humanized(attachment.body|length) }})
<a href="data:{{ attachment.contentType|default('application/octet-stream') }};base64,{{ collector.base64Encode(attachment.body) }}" download="{{ attachment.filename|default('attachment') }}">Download</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<div class="card-block">
<div class="sf-tabs sf-tabs-sm">
{% if message.htmlBody is defined %}
{% set textBody = message.textBody %}
{% set htmlBody = message.htmlBody %}
<div class="tab {{ not textBody ? 'disabled' }} {{ textBody ? 'active' }}">
<h3 class="tab-title">Text content</h3>
<div class="tab-content">
{% if textBody %}
<pre class="mailer-email-body prewrap" style="max-height: 600px">
{%- if message.textCharset() %}
{{- textBody|convert_encoding('UTF-8', message.textCharset()) }}
{%- else %}
{{- textBody }}
{%- endif -%}
</pre>
{% else %}
<div class="mailer-empty-email-body">
<p>The text body is empty.</p>
</div>
{% endif %}
</div>
</div>
{% if htmlBody %}
<div class="tab">
<h3 class="tab-title">HTML preview</h3>
<div class="tab-content">
<pre class="prewrap" style="max-height: 600px"><iframe src="data:text/html;charset=utf-8;base64,{{ collector.base64Encode(htmlBody) }}" style="height: 80vh;width: 100%;"></iframe>
</pre>
</div>
</div>
{% endif %}
<div class="tab {{ not htmlBody ? 'disabled' }} {{ not textBody and htmlBody ? 'active' }}">
<h3 class="tab-title">HTML content</h3>
<div class="tab-content">
{% if htmlBody %}
<pre class="mailer-email-body prewrap" style="max-height: 600px">
{%- if message.htmlCharset() %}
{{- htmlBody|convert_encoding('UTF-8', message.htmlCharset()) }}
{%- else %}
{{- htmlBody }}
{%- endif -%}
</pre>
{% else %}
<div class="mailer-empty-email-body">
<p>The HTML body is empty.</p>
</div>
{% endif %}
</div>
</div>
{% else %}
{% set body = message.body ? message.body.toString() : null %}
<div class="tab {{ not body ? 'disabled' }} {{ body ? 'active' }}">
<h3 class="tab-title">Content</h3>
<div class="tab-content">
{% if body %}
<pre class="mailer-email-body prewrap" style="max-height: 600px">
{{- body }}
</pre>
{% else %}
<div class="mailer-empty-email-body">
<p>The body is empty.</p>
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<div class="tab">
<h3 class="tab-title">MIME parts</h3>
<div class="tab-content">
<pre class="prewrap" style="max-height: 600px; margin-left: 5px">{{ message.body().asDebugString() }}</pre>
</div>
</div>
<div class="tab">
<h3 class="tab-title">Raw Message</h3>
<div class="tab-content">
<a class="mailer-message-download-raw" href="data:application/octet-stream;base64,{{ collector.base64Encode(message.toString()) }}" download="email.eml">
{{ source('@WebProfiler/Icon/download.svg') }}
Download as EML file
</a>
<pre class="prewrap" style="max-height: 600px; margin-left: 5px">{{ message.toString() }}</pre>
</div>
</div>
</div>
{% endif %}
{% endmacro %}
{% macro render_file_size_humanized(bytes) %}
{%- if bytes < 1000 -%}
{{- bytes ~ ' bytes' -}}
{%- elseif bytes < 1000 ** 2 -%}
{{- (bytes / 1000)|number_format(2) ~ ' kB' -}}
{%- else -%}
{{- (bytes / 1000 ** 2)|number_format(2) ~ ' MB' -}}
{%- endif -%}
{% endmacro %}
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% set icon %}
{% set status_color = (collector.memory / 1024 / 1024) > 50 ? 'yellow' %}
{{ source('@WebProfiler/Icon/memory.svg') }}
<span class="sf-toolbar-value">{{ '%.1f'|format(collector.memory / 1024 / 1024) }}</span>
<span class="sf-toolbar-label">MiB</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Peak memory usage</b>
<span>{{ '%.1f'|format(collector.memory / 1024 / 1024) }} MiB</span>
</div>
<div class="sf-toolbar-info-piece">
<b>PHP memory limit</b>
<span>{{ collector.memoryLimit == -1 ? 'Unlimited' : '%.0f MiB'|format(collector.memoryLimit / 1024 / 1024) }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, name: 'time', status: status_color }) }}
{% endblock %}

View File

@@ -0,0 +1,215 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block head %}
{{ parent() }}
<style>
.message-item thead th { position: relative; cursor: pointer; user-select: none; padding-right: 35px; }
.message-item tbody tr td:first-child { width: 170px; }
.message-item .label { float: right; padding: 1px 5px; opacity: .75; margin-left: 5px; }
.message-item .toggle-button { position: absolute; right: 6px; top: 6px; opacity: .5; pointer-events: none; color: inherit; }
.message-item .icon svg { height: 24px; width: 24px; }
.message-item .icon-close svg { transform: rotate(180deg); }
.message-item .sf-toggle-off .icon-close, .sf-toggle-on .icon-open { display: none; }
.message-item .sf-toggle-off .icon-open, .sf-toggle-on .icon-close { display: block; }
.message-bus .badge.status-some-errors { line-height: 16px; border-bottom: 2px solid #B0413E; }
.message-item tbody.sf-toggle-content.sf-toggle-visible { display: table-row-group; }
#collector-content .message-bus .trace {
border: var(--border);
background: var(--base-0);
padding: 10px;
margin: 0.5em 0;
overflow: auto;
}
#collector-content .message-bus .trace {
font-size: 12px;
}
#collector-content .message-bus .trace li {
margin-bottom: 0;
padding: 0;
}
#collector-content .message-bus .trace li.selected {
background: var(--highlight-selected-line);
}
</style>
{% endblock %}
{% block toolbar %}
{% if collector.messages|length > 0 %}
{% set status_color = collector.exceptionsCount ? 'red' %}
{% set icon %}
{{ source('@WebProfiler/Icon/messenger.svg') }}
<span class="sf-toolbar-value">{{ collector.messages|length }}</span>
{% endset %}
{% set text %}
{% for bus in collector.buses %}
{% set exceptionsCount = collector.exceptionsCount(bus) %}
<div class="sf-toolbar-info-piece">
<b>{{ bus }}</b>
<span
title="{{ exceptionsCount }} message(s) with exceptions"
class="sf-toolbar-status sf-toolbar-status-{{ exceptionsCount ? 'red' }}"
>
{{ collector.messages(bus)|length }}
</span>
</div>
{% endfor %}
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: 'messenger', status: status_color }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label{{ collector.exceptionsCount ? ' label-status-error' }}{{ collector.messages is empty ? ' disabled' }}">
<span class="icon">{{ source('@WebProfiler/Icon/messenger.svg') }}</span>
<strong>Messages</strong>
{% if collector.exceptionsCount > 0 %}
<span class="count">
<span>{{ collector.exceptionsCount }}</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
<h2>Messages</h2>
{% if collector.messages is empty %}
<div class="empty empty-panel">
<p>No messages have been collected.</p>
</div>
{% elseif 1 == collector.buses|length %}
<p class="text-muted">Ordered list of dispatched messages across all your buses</p>
{{ _self.render_bus_messages(collector.messages, true) }}
{% else %}
<div class="sf-tabs message-bus">
<div class="tab">
{% set messages = collector.messages %}
{% set exceptionsCount = collector.exceptionsCount %}
<h3 class="tab-title">All<span class="badge {{ exceptionsCount ? exceptionsCount == messages|length ? 'status-error' : 'status-some-errors' }}">{{ messages|length }}</span></h3>
<div class="tab-content">
<p class="text-muted">Ordered list of dispatched messages across all your buses</p>
{{ _self.render_bus_messages(messages, true) }}
</div>
</div>
{% for bus in collector.buses %}
<div class="tab message-bus">
{% set messages = collector.messages(bus) %}
{% set exceptionsCount = collector.exceptionsCount(bus) %}
<h3 class="tab-title">{{ bus }}<span class="badge {{ exceptionsCount ? exceptionsCount == messages|length ? 'status-error' : 'status-some-errors' }}">{{ messages|length }}</span></h3>
<div class="tab-content">
<p class="text-muted">Ordered list of messages dispatched on the <code>{{ bus }}</code> bus</p>
{{ _self.render_bus_messages(messages) }}
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% endblock %}
{% macro render_bus_messages(messages, showBus = false) %}
{% set discr = random() %}
{% for dispatchCall in messages %}
<table class="message-item">
<thead>
<tr>
<th colspan="2" class="sf-toggle"
data-toggle-selector="#message-item-{{ discr }}-{{ loop.index0 }}-details"
data-toggle-initial="{{ loop.first ? 'display' }}"
>
<span class="dump-inline">{{ profiler_dump(dispatchCall.message.type) }}</span>
{% if dispatchCall.exception is defined %}
<span class="label status-error">exception</span>
{% endif %}
<button class="btn btn-link toggle-button" type="button">
<span class="icon icon-close">{{ source('@WebProfiler/Icon/chevron-down.svg') }}</span>
<span class="icon icon-open">{{ source('@WebProfiler/Icon/chevron-down.svg') }}</span>
</button>
</th>
</tr>
</thead>
<tbody id="message-item-{{ discr }}-{{ loop.index0 }}-details" class="sf-toggle-content">
<tr>
<th scope="row" class="font-normal">Caller</th>
<td class="message-bus-dispatch-caller">
In
{% set caller = dispatchCall.caller %}
{% if caller.line %}
{% set link = caller.file|file_link(caller.line) %}
{% if link %}
<a href="{{ link }}" title="{{ caller.file }}">{{ caller.name }}</a>
{% else %}
<abbr title="{{ caller.file }}">{{ caller.name }}</abbr>
{% endif %}
{% else %}
{{ caller.name }}
{% endif %}
line <button type="button" class="btn-link text-small sf-toggle" data-toggle-selector="#sf-trace-{{ discr }}-{{ loop.index0 }}">{{ caller.line }}</button>
<div class="hidden" id="sf-trace-{{ discr }}-{{ loop.index0 }}">
<div class="trace">
{{ caller.file|file_excerpt(caller.line)|replace({
'#DD0000': 'var(--highlight-string)',
'#007700': 'var(--highlight-keyword)',
'#0000BB': 'var(--highlight-default)',
'#FF8000': 'var(--highlight-comment)'
})|raw }}
</div>
</div>
</td>
</tr>
{% if showBus %}
<tr>
<th scope="row" class="font-normal">Bus</th>
<td>{{ dispatchCall.bus }}</td>
</tr>
{% endif %}
<tr>
<th scope="row" class="font-normal">Message</th>
<td>{{ profiler_dump(dispatchCall.message.value, maxDepth=2) }}</td>
</tr>
<tr>
<th scope="row" class="font-normal">Envelope stamps <span class="block text-muted">when dispatching</span></th>
<td>
{% for item in dispatchCall.stamps %}
{{ profiler_dump(item) }}
{% else %}
<span class="text-muted font-normal">No items</span>
{% endfor %}
</td>
</tr>
{% if dispatchCall.stamps_after_dispatch is defined %}
<tr>
<th scope="row" class="font-normal">Envelope stamps <span class="block text-muted">after dispatch</span></th>
<td>
{% for item in dispatchCall.stamps_after_dispatch %}
{{ profiler_dump(item) }}
{% else %}
<span class="text-muted">No items</span>
{% endfor %}
</td>
</tr>
{% endif %}
{% if dispatchCall.exception is defined %}
<tr>
<td class="text-bold">Exception</td>
<td>
{{ profiler_dump(dispatchCall.exception.value, maxDepth=1) }}
</td>
</tr>
{% endif %}
</tbody>
</table>
{% endfor %}
{% endmacro %}

View File

@@ -0,0 +1,168 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% set events = collector.events %}
{% if events.messages|length %}
{% set icon %}
{{ source('@WebProfiler/Icon/notifier.svg') }}
<span class="sf-toolbar-value">{{ events.messages|length }}</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Sent notifications</b>
<span class="sf-toolbar-status">{{ events.messages|length }}</span>
</div>
{% for transport in events.transports %}
<div class="sf-toolbar-info-piece">
<b>{{ transport ?: '<em>Empty Transport Name</em>' }}</b>
<span class="sf-toolbar-status">{{ events.messages(transport)|length }}</span>
</div>
{% endfor %}
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { 'link': profiler_url }) }}
{% endif %}
{% endblock %}
{% block head %}
{{ parent() }}
<style>
/* utility classes */
.m-t-0 { margin-top: 0 !important; }
.m-t-10 { margin-top: 10px !important; }
/* basic grid */
.row {
display: flex;
flex-wrap: wrap;
margin-right: -15px;
margin-left: -15px;
}
.col {
flex-basis: 0;
flex-grow: 1;
max-width: 100%;
position: relative;
width: 100%;
min-height: 1px;
padding-right: 15px;
padding-left: 15px;
}
.col-4 {
flex: 0 0 33.333333%;
max-width: 33.333333%;
}
/* small tabs */
.sf-tabs-sm .tab-navigation li {
font-size: 14px;
padding: .3em .5em;
}
</style>
{% endblock %}
{% block menu %}
{% set events = collector.events %}
<span class="label {{ events.messages|length ? '' : 'disabled' }}">
<span class="icon">{{ source('@WebProfiler/Icon/notifier.svg') }}</span>
<strong>Notifications</strong>
{% if events.messages|length > 0 %}
<span class="count">
<span>{{ events.messages|length }}</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
{% set events = collector.events %}
<h2>Notifications</h2>
{% if not events.messages|length %}
<div class="empty empty-panel">
<p>No notifications were sent.</p>
</div>
{% endif %}
<div class="metrics">
{% for transport in events.transports %}
<div class="metric">
<span class="value">{{ events.messages(transport)|length }}</span>
<span class="label">{{ transport }}</span>
</div>
{% endfor %}
</div>
{% for transport in events.transports %}
<h3>{{ transport ?: '<em>Empty Transport Name</em>' }}</h3>
<div class="card-block">
<div class="sf-tabs sf-tabs-sm">
{% for event in events.events(transport) %}
{% set message = event.message %}
<div class="tab">
<h3 class="tab-title">Message #{{ loop.index }} <small>({{ event.isQueued() ? 'queued' : 'sent' }})</small></h3>
<div class="tab-content">
<div class="card">
<div class="card-block">
<span class="label">Subject</span>
<h2 class="m-t-10">{{ message.getSubject() ?? '(empty)' }}</h2>
</div>
{% set notification = message.notification ?? null %}
{% if notification %}
<div class="card-block">
<div class="row">
<div class="col">
<span class="label">Content</span>
<pre class="prewrap">{{ notification.getContent() ?? '(empty)' }}</pre>
<span class="label">Importance</span>
<pre class="prewrap">{{ notification.getImportance() }}</pre>
</div>
</div>
</div>
{% endif %}
<div class="card-block">
<div class="sf-tabs sf-tabs-sm">
{% if notification %}
<div class="tab">
<h3 class="tab-title">Notification</h3>
<div class="tab-content">
<pre class="prewrap" style="max-height: 600px">
{{- 'Subject: ' ~ notification.getSubject() }}<br/>
{{- 'Content: ' ~ notification.getContent() }}<br/>
{{- 'Importance: ' ~ notification.getImportance() }}<br/>
{{- 'Emoji: ' ~ (notification.getEmoji() is empty ? '(empty)' : notification.getEmoji()) }}<br/>
{{- 'Exception: ' ~ (notification.getException() ?? '(empty)') }}<br/>
{{- 'ExceptionAsString: ' ~ (notification.getExceptionAsString() is empty ? '(empty)' : notification.getExceptionAsString()) }}
</pre>
</div>
</div>
{% endif %}
<div class="tab">
<h3 class="tab-title">Message Options</h3>
<div class="tab-content">
<pre class="prewrap" style="max-height: 600px">
{%- if message.getOptions() is null %}
{{- '(empty)' }}
{%- else %}
{{- message.getOptions().toArray()|json_encode(constant('JSON_PRETTY_PRINT')) }}
{%- endif %}
</pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
{% endblock %}

View File

@@ -0,0 +1,431 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block head %}
{{ parent() }}
<style>
.empty-query-post-files {
display: flex;
justify-content: space-between;
}
.empty-query-post-files > div {
flex: 1;
}
.empty-query-post-files > div + div {
margin-left: 30px;
}
.empty-query-post-files h3 {
margin-top: 0;
}
.empty-query-post-files .empty {
margin-bottom: 0;
}
</style>
{% endblock %}
{% block toolbar %}
{% set request_handler %}
{{ _self.set_handler(collector.controller) }}
{% endset %}
{% if collector.redirect %}
{% set redirect_handler %}
{{ _self.set_handler(collector.redirect.controller, collector.redirect.route, 'GET' != collector.redirect.method ? collector.redirect.method) }}
{% endset %}
{% endif %}
{% if collector.forwardtoken %}
{% set forward_profile = profile.childByToken(collector.forwardtoken) %}
{% set forward_handler %}
{{ _self.set_handler(forward_profile ? forward_profile.collector('request').controller : 'n/a') }}
{% endset %}
{% endif %}
{% set request_status_code_color = (collector.statuscode >= 400) ? 'red' : (collector.statuscode >= 300) ? 'yellow' : 'green' %}
{% set icon %}
<span class="sf-toolbar-status sf-toolbar-status-{{ request_status_code_color }}">{{ collector.statuscode }}</span>
{% if collector.route %}
{% if collector.redirect %}<span class="sf-toolbar-request-icon">{{ source('@WebProfiler/Icon/redirect.svg') }}</span>{% endif %}
{% if collector.forwardtoken %}<span class="sf-toolbar-request-icon">{{ source('@WebProfiler/Icon/forward.svg') }}</span>{% endif %}
<span class="sf-toolbar-label">{{ 'GET' != collector.method ? collector.method }} @</span>
<span class="sf-toolbar-value sf-toolbar-info-piece-additional">{{ collector.route }}</span>
{% endif %}
{% endset %}
{% set text %}
<div class="sf-toolbar-info-group">
<div class="sf-toolbar-info-piece">
<b>HTTP status</b>
<span>{{ collector.statuscode }} {{ collector.statustext }}</span>
</div>
{% if 'GET' != collector.method -%}
<div class="sf-toolbar-info-piece">
<b>Method</b>
<span>{{ collector.method }}</span>
</div>
{%- endif %}
<div class="sf-toolbar-info-piece">
<b>Controller</b>
<span>{{ request_handler }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Route name</b>
<span>{{ collector.route|default('n/a') }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Has session</b>
<span>{% if collector.sessionmetadata|length %}yes{% else %}no{% endif %}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Stateless Check</b>
<span>{% if collector.statelesscheck %}yes{% else %}no{% endif %}</span>
</div>
</div>
{% if redirect_handler is defined -%}
<div class="sf-toolbar-info-group">
<div class="sf-toolbar-info-piece">
<b>
<span class="sf-toolbar-redirection-status sf-toolbar-status-yellow">{{ collector.redirect.status_code }}</span>
Redirect from
</b>
<span>
{{ redirect_handler }}
(<a href="{{ path('_profiler', { token: collector.redirect.token }) }}">{{ collector.redirect.token }}</a>)
</span>
</div>
</div>
{% endif %}
{% if forward_handler is defined %}
<div class="sf-toolbar-info-group">
<div class="sf-toolbar-info-piece">
<b>Forwarded to</b>
<span>
{{ forward_handler }}
(<a href="{{ path('_profiler', { token: collector.forwardtoken }) }}">{{ collector.forwardtoken }}</a>)
</span>
</div>
</div>
{% endif %}
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }}
{% endblock %}
{% block menu %}
<span class="label">
<span class="icon">{{ source('@WebProfiler/Icon/request.svg') }}</span>
<strong>Request / Response</strong>
</span>
{% endblock %}
{% block panel %}
{% set controller_name = _self.set_handler(collector.controller) %}
<h2>
{{ 'n/a' in controller_name ? 'Request / Response' : controller_name }}
</h2>
<div class="sf-tabs">
<div class="tab">
<h3 class="tab-title">Request</h3>
<div class="tab-content">
{% set has_no_query_post_or_files = collector.requestquery.all is empty and collector.requestrequest.all is empty and collector.requestfiles is empty %}
{% if has_no_query_post_or_files %}
<div class="empty-query-post-files" style="display: flex; align-items: stretch">
<div>
<h3>GET Parameters</h3>
<div class="empty"><p>None</p></div>
</div>
<div>
<h3>POST Parameters</h3>
<div class="empty"><p>None</p></div>
</div>
<div>
<h3>Uploaded Files</h3>
<div class="empty"><p>None</p></div>
</div>
</div>
{% else %}
<h3>GET Parameters</h3>
{% if collector.requestquery.all is empty %}
<div class="empty">
<p>No GET parameters</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestquery, maxDepth: 1 }, with_context = false) }}
{% endif %}
<h3>POST Parameters</h3>
{% if collector.requestrequest.all is empty %}
<div class="empty">
<p>No POST parameters</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestrequest, maxDepth: 1 }, with_context = false) }}
{% endif %}
<h4>Uploaded Files</h4>
{% if collector.requestfiles is empty %}
<div class="empty">
<p>No files were uploaded</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestfiles, maxDepth: 1 }, with_context = false) }}
{% endif %}
{% endif %}
<h3>Request Attributes</h3>
{% if collector.requestattributes.all is empty %}
<div class="empty">
<p>No attributes</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestattributes }, with_context = false) }}
{% endif %}
<h3>Request Headers</h3>
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestheaders, labels: ['Header', 'Value'], maxDepth: 1 }, with_context = false) }}
<h3>Request Content</h3>
{% if collector.content == false %}
<div class="empty">
<p>Request content not available (it was retrieved as a resource).</p>
</div>
{% elseif collector.content %}
<div class="sf-tabs">
{% set prettyJson = collector.isJsonRequest ? collector.prettyJson : null %}
{% if prettyJson is not null %}
<div class="tab">
<h3 class="tab-title">Pretty</h3>
<div class="tab-content">
<div class="card" style="max-height: 500px; overflow-y: auto;">
<pre class="break-long-words">{{ prettyJson }}</pre>
</div>
</div>
</div>
{% endif %}
<div class="tab">
<h3 class="tab-title">Raw</h3>
<div class="tab-content">
<div class="card">
<pre class="break-long-words">{{ collector.content }}</pre>
</div>
</div>
</div>
</div>
{% else %}
<div class="empty">
<p>No content</p>
</div>
{% endif %}
</div>
</div>
<div class="tab">
<h3 class="tab-title">Response</h3>
<div class="tab-content">
<h3>Response Headers</h3>
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.responseheaders, labels: ['Header', 'Value'], maxDepth: 1 }, with_context = false) }}
</div>
</div>
<div class="tab {{ collector.requestcookies.all is empty and collector.responsecookies.all is empty ? 'disabled' }}">
<h3 class="tab-title">Cookies</h3>
<div class="tab-content">
<h3>Request Cookies</h3>
{% if collector.requestcookies.all is empty %}
<div class="empty">
<p>No request cookies</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.requestcookies }, with_context = false) }}
{% endif %}
<h3>Response Cookies</h3>
{% if collector.responsecookies.all is empty %}
<div class="empty">
<p>No response cookies</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.responsecookies }, with_context = true) }}
{% endif %}
</div>
</div>
<div class="tab {{ collector.sessionmetadata is empty ? 'disabled' }}">
<h3 class="tab-title">Session{% if collector.sessionusages is not empty %} <span class="badge">{{ collector.sessionusages|length }}</span>{% endif %}</h3>
<div class="tab-content">
<h3>Session Metadata</h3>
{% if collector.sessionmetadata is empty %}
<div class="empty">
<p>No session metadata</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/table.html.twig', { data: collector.sessionmetadata }, with_context = false) }}
{% endif %}
<h3>Session Attributes</h3>
{% if collector.sessionattributes is empty %}
<div class="empty">
<p>No session attributes</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/table.html.twig', { data: collector.sessionattributes, labels: ['Attribute', 'Value'] }, with_context = false) }}
{% endif %}
<h3>Session Usage</h3>
<div class="metrics">
<div class="metric">
<span class="value">{{ collector.sessionusages|length }}</span>
<span class="label">Usages</span>
</div>
<div class="metric">
<span class="value">{{ source('@WebProfiler/Icon/' ~ (collector.statelesscheck ? 'yes' : 'no') ~ '.svg') }}</span>
<span class="label">Stateless check enabled</span>
</div>
</div>
{% if collector.sessionusages is empty %}
<div class="empty">
<p>Session not used.</p>
</div>
{% else %}
<table class="session_usages">
<thead>
<tr>
<th class="full-width">Usage</th>
</tr>
</thead>
<tbody>
{% for key, usage in collector.sessionusages %}
<tr>
<td class="font-normal">
{%- set link = usage.file|file_link(usage.line) %}
{%- if link %}<a href="{{ link }}" title="{{ usage.name }}">{% else %}<span title="{{ usage.name }}">{% endif %}
{{ usage.name }}
{%- if link %}</a>{% else %}</span>{% endif %}
<div class="text-small font-normal">
{% set usage_id = 'session-usage-trace-' ~ key %}
<a class="btn btn-link text-small sf-toggle" data-toggle-selector="#{{ usage_id }}" data-toggle-alt-content="Hide trace">Show trace</a>
</div>
<div id="{{ usage_id }}" class="context sf-toggle-content sf-toggle-hidden">
{{ profiler_dump(usage.trace, maxDepth=2) }}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
<div class="tab {{ collector.flashes is empty ? 'disabled' }}">
<h3 class="tab-title">Flashes</h3>
<div class="tab-content">
<h3>Flashes</h3>
{% if collector.flashes is empty %}
<div class="empty">
<p>No flash messages were created.</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/table.html.twig', { data: collector.flashes }, with_context = false) }}
{% endif %}
</div>
</div>
<div class="tab">
<h3 class="tab-title">Server Parameters</h3>
<div class="tab-content">
<h3>Server Parameters</h3>
<h4>Defined in .env</h4>
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: collector.dotenvvars }, with_context = false) }}
<h4>Defined as regular env variables</h4>
{% set requestserver = [] %}
{% for key, value in collector.requestserver|filter((_, key) => key not in collector.dotenvvars.keys) %}
{% set requestserver = requestserver|merge({(key): value}) %}
{% endfor %}
{{ include('@WebProfiler/Profiler/table.html.twig', { data: requestserver }, with_context = false) }}
</div>
</div>
{% if profile.parent %}
<div class="tab">
<h3 class="tab-title">Parent Request</h3>
<div class="tab-content">
<h3>
<a href="{{ path('_profiler', { token: profile.parent.token }) }}">Return to parent request</a>
<small>(token = {{ profile.parent.token }})</small>
</h3>
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: profile.parent.getcollector('request').requestattributes }, with_context = false) }}
</div>
</div>
{% endif %}
{% if profile.children|length %}
<div class="tab">
<h3 class="tab-title">Sub Requests <span class="badge">{{ profile.children|length }}</span></h3>
<div class="tab-content">
{% for child in profile.children %}
<h3>
{{ _self.set_handler(child.getcollector('request').controller) }}
<small>(token = <a href="{{ path('_profiler', { token: child.token }) }}">{{ child.token }}</a>)</small>
</h3>
{{ include('@WebProfiler/Profiler/bag.html.twig', { bag: child.getcollector('request').requestattributes }, with_context = false) }}
{% endfor %}
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% macro set_handler(controller, route, method) %}
{% if controller.class is defined -%}
{%- if method|default(false) %}<span class="sf-toolbar-status sf-toolbar-redirection-method">{{ method }}</span>{% endif -%}
{%- set link = controller.file|file_link(controller.line) %}
{%- if link %}<a href="{{ link }}" title="{{ controller.class }}">{% else %}<span title="{{ controller.class }}">{% endif %}
{%- if route|default(false) -%}
@{{ route }}
{%- else -%}
{{- controller.class|abbr_class|striptags -}}
{{- controller.method ? ' :: ' ~ controller.method -}}
{%- endif -%}
{%- if link %}</a>{% else %}</span>{% endif %}
{%- else -%}
<span>{{ route|default(controller) }}</span>
{%- endif %}
{% endmacro %}

View File

@@ -0,0 +1,14 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}{% endblock %}
{% block menu %}
<span class="label">
<span class="icon">{{ source('@WebProfiler/Icon/router.svg') }}</span>
<strong>Routing</strong>
</span>
{% endblock %}
{% block panel %}
{{ render(controller('web_profiler.controller.router::panelAction', { token: token })) }}
{% endblock %}

View File

@@ -0,0 +1,370 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block head %}
{{ parent() }}
<style>
#collector-content .sf-serializer {
margin-bottom: 2em;
}
#collector-content .sf-serializer .trace {
border: var(--border);
background: var(--page-background);
padding: 10px;
margin: 0.5em 0;
overflow: auto;
}
#collector-content .sf-serializer .trace {
font-size: 12px;
}
#collector-content .sf-serializer .trace li {
margin-bottom: 0;
padding: 0;
}
#collector-content .sf-serializer .trace li.selected {
background: var(--highlight-selected-line);
}
</style>
{% endblock %}
{% block toolbar %}
{% if collector.handledCount > 0 %}
{% set icon %}
{{ source('@WebProfiler/Icon/serializer.svg') }}
<span class="sf-toolbar-value">
{{ collector.handledCount }}
</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Total calls</b>
<span class="sf-toolbar-status">{{ collector.handledCount }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Total time</b>
<span>
{{ '%.2f'|format(collector.totalTime * 1000) }} <span class="unit">ms</span>
</span>
</div>
<div class="detailed-metrics">
<div>
<div class="sf-toolbar-info-piece">
<b>Serialize</b>
<span class="sf-toolbar-status">{{ collector.data.serialize|length }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Deserialize</b>
<span class="sf-toolbar-status">{{ collector.data.deserialize|length }}</span>
</div>
</div>
<div>
<div class="sf-toolbar-info-piece">
<b>Encode</b>
<span class="sf-toolbar-status">{{ collector.data.encode|length }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Decode</b>
<span class="sf-toolbar-status">{{ collector.data.decode|length }}</span>
</div>
</div>
<div>
<div class="sf-toolbar-info-piece">
<b>Normalize</b>
<span class="sf-toolbar-status">{{ collector.data.normalize|length }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Denormalize</b>
<span class="sf-toolbar-status">{{ collector.data.denormalize|length }}</span>
</div>
</div>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label {{ not collector.handledCount ? 'disabled' }}">
<span class="icon">{{ source('@WebProfiler/Icon/serializer.svg') }}</span>
<strong>Serializer</strong>
</span>
{% endblock %}
{% block panel %}
<h2>Serializer</h2>
<div class="sf-serializer sf-reset">
{% if not collector.handledCount %}
<div class="empty empty-panel">
<p>Nothing was handled by the serializer.</p>
</div>
{% else %}
<div class="metrics">
<div class="metric">
<span class="value">{{ collector.handledCount }}</span>
<span class="label">Handled</span>
</div>
<div class="metric">
<span class="value">{{ '%.2f'|format(collector.totalTime * 1000) }} <span class="unit">ms</span></span>
<span class="label">Total time</span>
</div>
</div>
<div class="sf-tabs">
{% for serializer in collector.serializerNames %}
{{ _self.render_serializer_tab(collector, serializer) }}
{% endfor %}
</div>
{% endif %}
</div>
{% endblock %}
{% macro render_serializer_tab(collector, serializer) %}
<div class="tab">
<h3 class="tab-title">{{ serializer }} <span class="badge">{{ collector.handledCount(serializer) }}</h3>
<div class="tab-content">
<div class="sf-tabs">
{{ _self.render_serialize_tab(collector.data(serializer), true, serializer) }}
{{ _self.render_serialize_tab(collector.data(serializer), false, serializer) }}
{{ _self.render_normalize_tab(collector.data(serializer), true, serializer) }}
{{ _self.render_normalize_tab(collector.data(serializer), false, serializer) }}
{{ _self.render_encode_tab(collector.data(serializer), true, serializer) }}
{{ _self.render_encode_tab(collector.data(serializer), false, serializer) }}
</div>
</div>
</div>
{% endmacro %}
{% macro render_serialize_tab(collectorData, serialize, serializer) %}
{% set data = serialize ? collectorData.serialize : collectorData.deserialize %}
{% set cellPrefix = serialize ? 'serialize' : 'deserialize' %}
<div class="tab {{ not data ? 'disabled' }}">
<h3 class="tab-title">{{ serialize ? 'serialize' : 'deserialize' }} <span class="badge">{{ data|length }}</h3>
<div class="tab-content">
{% if not data|length %}
<div class="empty">
<p>Nothing was {{ serialize ? 'serialized' : 'deserialized' }}.</p>
</div>
{% else %}
<table>
<thead>
<tr>
<th>Data</th>
<th>Context</th>
<th>Normalizer</th>
<th>Encoder</th>
<th>Time</th>
<th>Caller</th>
</tr>
</thead>
<tbody>
{% for item in data %}
<tr>
<td>{{ _self.render_data_cell(item, loop.index, cellPrefix, serializer) }}</td>
<td>{{ _self.render_context_cell(item, loop.index, cellPrefix, serializer) }}</td>
<td>{{ _self.render_normalizer_cell(item, loop.index, cellPrefix, serializer) }}</td>
<td>{{ _self.render_encoder_cell(item, loop.index, cellPrefix, serializer) }}</td>
<td>{{ _self.render_time_cell(item) }}</td>
<td>{{ _self.render_caller_cell(item, loop.index, cellPrefix, serializer) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
{% endmacro %}
{% macro render_caller_cell(item, index, method, serializer) %}
{% if item.caller is defined %}
{% set trace_id = 'sf-trace-' ~ serializer ~ '-' ~ method ~ '-' ~ index %}
<span class="metadata">
{% set caller = item.caller %}
{% if caller.line %}
{% set link = caller.file|file_link(caller.line) %}
{% if link %}
<a href="{{ link }}" title="{{ caller.file }}">{{ caller.name }}</a>
{% else %}
<abbr title="{{ caller.file }}">{{ caller.name }}</abbr>
{% endif %}
{% else %}
{{ caller.name }}
{% endif %}
line <a class="text-small sf-toggle" data-toggle-selector="#{{ trace_id }}">{{ caller.line }}</a>
</span>
<div class="sf-serializer-compact hidden" id="{{ trace_id }}">
<div class="trace">
{{ caller.file|file_excerpt(caller.line)|replace({
'#DD0000': 'var(--highlight-string)',
'#007700': 'var(--highlight-keyword)',
'#0000BB': 'var(--highlight-default)',
'#FF8000': 'var(--highlight-comment)'
})|raw }}
</div>
</div>
{% endif %}
{% endmacro %}
{% macro render_normalize_tab(collectorData, normalize, serializer) %}
{% set data = normalize ? collectorData.normalize : collectorData.denormalize %}
{% set cellPrefix = normalize ? 'normalize' : 'denormalize' %}
<div class="tab {{ not data ? 'disabled' }}">
<h3 class="tab-title">{{ normalize ? 'normalize' : 'denormalize' }} <span class="badge">{{ data|length }}</h3>
<div class="tab-content">
{% if not data|length %}
<div class="empty">
<p>Nothing was {{ normalize ? 'normalized' : 'denormalized' }}.</p>
</div>
{% else %}
<table>
<thead>
<tr>
<th>Data</th>
<th>Context</th>
<th>Normalizer</th>
<th>Time</th>
<th>Caller</th>
</tr>
</thead>
<tbody>
{% for item in data %}
<tr>
<td>{{ _self.render_data_cell(item, loop.index, cellPrefix, serializer) }}</td>
<td>{{ _self.render_context_cell(item, loop.index, cellPrefix, serializer) }}</td>
<td>{{ _self.render_normalizer_cell(item, loop.index, cellPrefix, serializer) }}</td>
<td>{{ _self.render_time_cell(item) }}</td>
<td>{{ _self.render_caller_cell(item, loop.index, cellPrefix, serializer) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
{% endmacro %}
{% macro render_encode_tab(collectorData, encode, serializer) %}
{% set data = encode ? collectorData.encode : collectorData.decode %}
{% set cellPrefix = encode ? 'encode' : 'decode' %}
<div class="tab {{ not data ? 'disabled' }}">
<h3 class="tab-title">{{ encode ? 'encode' : 'decode' }} <span class="badge">{{ data|length }}</h3>
<div class="tab-content">
{% if not data|length %}
<div class="empty">
<p>Nothing was {{ encode ? 'encoded' : 'decoded' }}.</p>
</div>
{% else %}
<table>
<thead>
<tr>
<th>Data</th>
<th>Context</th>
<th>Encoder</th>
<th>Time</th>
<th>Caller</th>
</tr>
</thead>
<tbody>
{% for item in data %}
<tr>
<td>{{ _self.render_data_cell(item, loop.index, cellPrefix, serializer) }}</td>
<td>{{ _self.render_context_cell(item, loop.index, cellPrefix, serializer) }}</td>
<td>{{ _self.render_encoder_cell(item, loop.index, cellPrefix, serializer) }}</td>
<td>{{ _self.render_time_cell(item) }}</td>
<td>{{ _self.render_caller_cell(item, loop.index, cellPrefix, serializer) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
{% endmacro %}
{% macro render_data_cell(item, index, method, serializer) %}
{% set data_id = 'data-' ~ serializer ~ '-' ~ method ~ '-' ~ index %}
<span class="nowrap">{{ item.dataType }}</span>
<div>
<a class="btn btn-link text-small sf-toggle" data-toggle-selector="#{{ data_id }}" data-toggle-alt-content="Hide contents">Show contents</a>
<div id="{{ data_id }}" class="context sf-toggle-content sf-toggle-hidden">
{{ profiler_dump(item.data) }}
</div>
</div>
{% endmacro %}
{% macro render_context_cell(item, index, method, serializer) %}
{% set context_id = 'context-' ~ serializer ~ '-' ~ method ~ '-' ~ index %}
{% if item.type %}
<span class="nowrap">Type: {{ item.type }}</span>
<div class="nowrap">Format: {{ item.format ? item.format : 'none' }}</div>
{% else %}
<span class="nowrap">Format: {{ item.format ? item.format : 'none' }}</span>
{% endif %}
<div>
<a class="btn btn-link text-small sf-toggle" data-toggle-selector="#{{ context_id }}" data-toggle-alt-content="Hide context">Show context</a>
<div id="{{ context_id }}" class="context sf-toggle-content sf-toggle-hidden">
{{ profiler_dump(item.context) }}
</div>
</div>
{% endmacro %}
{% macro render_normalizer_cell(item, index, method, serializer) %}
{% set nested_normalizers_id = 'nested-normalizers-' ~ serializer ~ '-' ~ method ~ '-' ~ index %}
{% if item.normalizer is defined %}
<span class="nowrap"><a href="{{ item.normalizer.file|file_link(item.normalizer.line) }}" title="{{ item.normalizer.file }}">{{ item.normalizer.class }}</a> ({{ '%.2f'|format(item.normalizer.time * 1000) }} ms)</span>
{% endif %}
{% if item.normalization|length > 1 %}
<div>
<a class="btn btn-link text-small sf-toggle" data-toggle-selector="#{{ nested_normalizers_id }}" data-toggle-alt-content="Hide nested normalizers">Show nested normalizers</a>
<div id="{{ nested_normalizers_id }}" class="context sf-toggle-content sf-toggle-hidden">
<ul class="text-small" style="line-height:80%;margin-top:10px">
{% for normalizer in item.normalization %}
<li><span class="nowrap">x{{ normalizer.calls }} <a href="{{ normalizer.file|file_link(normalizer.line) }}" title="{{ normalizer.file }}">{{ normalizer.class }}</a> ({{ '%.2f'|format(normalizer.time * 1000) }} ms)</span></li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% endmacro %}
{% macro render_encoder_cell(item, index, method, serializer) %}
{% set nested_encoders_id = 'nested-encoders-' ~ serializer ~ '-' ~ method ~ '-' ~ index %}
{% if item.encoder is defined %}
<span class="nowrap"><a href="{{ item.encoder.file|file_link(item.encoder.line) }}" title="{{ item.encoder.file }}">{{ item.encoder.class }}</a> ({{ '%.2f'|format(item.encoder.time * 1000) }} ms)</span>
{% endif %}
{% if item.encoding|length > 1 %}
<div>
<a class="btn btn-link text-small sf-toggle" data-toggle-selector="#{{ nested_encoders_id }}" data-toggle-alt-content="Hide nested encoders">Show nested encoders</a>
<div id="{{ nested_encoders_id }}" class="context sf-toggle-content sf-toggle-hidden">
<ul class="text-small" style="line-height:80%;margin-top:10px">
{% for encoder in item.encoding %}
<li><span class="nowrap">x{{ encoder.calls }} <a href="{{ encoder.file|file_link(encoder.line) }}" title="{{ encoder.file }}">{{ encoder.class }}</a> ({{ '%.2f'|format(encoder.time * 1000) }} ms)</span></li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% endmacro %}
{% macro render_time_cell(item) %}
<span class="nowrap">{{ '%.2f'|format(item.time * 1000) }} ms</span>
{% endmacro %}

View File

@@ -0,0 +1,67 @@
/* Legend */
.sf-profiler-timeline .legends .timeline-category {
border: none;
background: none;
border-left: 1em solid transparent;
line-height: 1em;
margin: 0 1em 0 0;
padding: 0 0.5em;
display: none;
opacity: 0.5;
}
.sf-profiler-timeline .legends .timeline-category.active {
opacity: 1;
}
.sf-profiler-timeline .legends .timeline-category.present {
display: inline-block;
}
.timeline-graph {
margin: 1em 0;
width: 100%;
background-color: var(--table-background);
border: 1px solid var(--table-border-color);
}
/* Typography */
.timeline-graph .timeline-label {
font-family: var(--font-sans-serif);
font-size: 12px;
line-height: 12px;
font-weight: normal;
fill: var(--color-text);
}
.timeline-graph .timeline-label .timeline-sublabel {
margin-left: 1em;
fill: var(--color-muted);
}
.timeline-graph .timeline-subrequest,
.timeline-graph .timeline-border {
fill: none;
stroke: var(--table-border-color);
stroke-width: 1px;
}
.timeline-graph .timeline-subrequest {
fill: url(#subrequest);
fill-opacity: 0.5;
}
.timeline-subrequest-pattern {
fill: var(--gray-200);
}
.theme-dark .timeline-subrequest-pattern {
fill: var(--gray-600);
}
/* Timeline periods */
.timeline-graph .timeline-period {
stroke-width: 0;
}

View File

@@ -0,0 +1,249 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block head %}
{{ parent() }}
<style>
#timeline-control {
background: var(--table-background);
box-shadow: var(--shadow);
margin: 1em 0;
padding: 10px;
}
#timeline-control label {
font-weight: bold;
margin-right: 1em;
}
#timeline-control input {
background: var(--metric-value-background);
font-size: 16px;
padding: 4px;
text-align: right;
width: 5em;
}
#timeline-control .help {
margin-left: 1em;
}
.sf-profiler-timeline .legends {
font-size: 12px;
line-height: 1.5em;
}
.sf-profiler-timeline .legends button {
color: var(--color-text);
}
.sf-profiler-timeline + p.help {
margin-top: 0;
}
</style>
{% endblock %}
{% block toolbar %}
{% set has_time_events = collector.events|length > 0 %}
{% set total_time = has_time_events ? '%.0f'|format(collector.duration) : 'n/a' %}
{% set initialization_time = collector.events|length ? '%.0f'|format(collector.inittime) : 'n/a' %}
{% set status_color = has_time_events and collector.duration > 1000 ? 'yellow' %}
{% set icon %}
{{ source('@WebProfiler/Icon/time.svg') }}
<span class="sf-toolbar-value">{{ total_time }}</span>
<span class="sf-toolbar-label">ms</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Total time</b>
<span>{{ total_time }} ms</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Initialization time</b>
<span>{{ initialization_time }} ms</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }}
{% endblock %}
{% block menu %}
<span class="label">
<span class="icon">{{ source('@WebProfiler/Icon/time.svg') }}</span>
<strong>Performance</strong>
</span>
{% endblock %}
{% block panel %}
{% set has_time_events = collector.events|length > 0 %}
<h2>Performance metrics</h2>
<div class="metrics">
<div class="metric-group">
<div class="metric">
<span class="value">{{ '%.0f'|format(collector.duration) }} <span class="unit">ms</span></span>
<span class="label">Total execution time</span>
</div>
<div class="metric">
<span class="value">{{ '%.0f'|format(collector.inittime) }} <span class="unit">ms</span></span>
<span class="label">Symfony initialization</span>
</div>
</div>
{% if profile.collectors.memory %}
<div class="metric-divider"></div>
<div class="metric">
<span class="value">{{ '%.2f'|format(profile.collectors.memory.memory / 1024 / 1024) }} <span class="unit">MiB</span></span>
<span class="label">Peak memory usage</span>
</div>
{% endif %}
{% if profile.children|length > 0 %}
<div class="metric-divider"></div>
<div class="metric-group">
<div class="metric">
<span class="value">{{ profile.children|length }}</span>
<span class="label">Sub-{{ profile_type|title }}{{ profile.children|length > 1 ? 's' }}</span>
</div>
{% set subrequests_time = has_time_events
? profile.children|reduce((total, child) => total + child.getcollector('time').events.__section__.duration, 0)
: 'n/a' %}
<div class="metric">
<span class="value">{{ subrequests_time }} <span class="unit">ms</span></span>
<span class="label">Sub-{{ profile_type|title }}{{ profile.children|length > 1 ? 's' }} time</span>
</div>
</div>
{% endif %}
</div>
<h2>Execution timeline</h2>
{% if not collector.isStopwatchInstalled() %}
<div class="empty empty-panel">
<p>The Stopwatch component is not installed. If you want to see timing events, run: <code>composer require symfony/stopwatch</code>.</p>
</div>
{% elseif collector.events is empty %}
<div class="empty">
<p>No timing events have been recorded. Check that symfony/stopwatch is installed and debugging enabled in the kernel.</p>
</div>
{% else %}
{{ block('panelContent') }}
{% endif %}
{% endblock %}
{% block panelContent %}
<form id="timeline-control" action="" method="get">
<input type="hidden" name="panel" value="time">
<label for="threshold">Threshold</label>
<input type="number" name="threshold" id="threshold" value="1" min="0" placeholder="1.1"> ms
<span class="help">(timeline only displays events with a duration longer than this threshold)</span>
</form>
{% if profile.parent %}
<h3 class="dump-inline">
Sub-{{ profile_type|title }} {{ profiler_dump(profile.getcollector('request').requestattributes.get('_controller')) }}
<small>
{{ collector.events.__section__.duration }} ms
<a class="newline" href="{{ path('_profiler', { token: profile.parent.token, panel: 'time' }) }}">Return to parent {{ profile_type }}</a>
</small>
</h3>
{% elseif profile.children|length > 0 %}
<h3>
Main {{ profile_type|title }} <small>{{ collector.events.__section__.duration }} ms</small>
</h3>
{% endif %}
{{ _self.display_timeline(token, collector.events, collector.events.__section__.origin) }}
{% if profile.children|length %}
<p class="help">Note: sections with a striped background correspond to sub-{{ profile_type }}s.</p>
<h3>Sub-{{ profile_type }}s <small>({{ profile.children|length }})</small></h3>
{% for child in profile.children %}
{% set events = child.getcollector('time').events %}
<h4>
<a href="{{ path('_profiler', { token: child.token, panel: 'time' }) }}">{{ child.getcollector('request').identifier }}</a>
<small>{{ events.__section__.duration }} ms</small>
</h4>
{{ _self.display_timeline(child.token, events, collector.events.__section__.origin) }}
{% endfor %}
{% endif %}
<svg id="timeline-template" width="0" height="0">
<defs>
<pattern id="subrequest" class="timeline-subrequest-pattern" patternUnits="userSpaceOnUse" width="20" height="20" viewBox="0 0 40 40">
<path d="M0 40L40 0H20L0 20M40 40V20L20 40"/>
</pattern>
</defs>
</svg>
<style>
{{ include('@WebProfiler/Collector/time.css.twig') }}
</style>
<script>
{{ source('@WebProfiler/Collector/time.js') }}
</script>
{% endblock %}
{% macro dump_request_data(token, events, origin) %}
{% autoescape 'js' %}
{
id: "{{ token }}",
left: {{ "%F"|format(events.__section__.origin - origin) }},
end: "{{ '%F'|format(events.__section__.endtime) }}",
events: [ {{ _self.dump_events(events) }} ],
}
{% endautoescape %}
{% endmacro %}
{% macro dump_events(events) %}
{% autoescape 'js' %}
{% for name, event in events %}
{% if '__section__' != name %}
{
name: "{{ name }}",
category: "{{ event.category }}",
origin: {{ "%F"|format(event.origin) }},
starttime: {{ "%F"|format(event.starttime) }},
endtime: {{ "%F"|format(event.endtime) }},
duration: {{ "%F"|format(event.duration) }},
memory: {{ "%.1F"|format(event.memory / 1024 / 1024) }},
elements: {},
periods: [
{%- for period in event.periods -%}
{
start: {{ "%F"|format(period.starttime) }},
end: {{ "%F"|format(period.endtime) }},
duration: {{ "%F"|format(period.duration) }},
elements: {}
},
{%- endfor -%}
],
},
{% endif %}
{% endfor %}
{% endautoescape %}
{% endmacro %}
{% macro display_timeline(token, events, origin) %}
<div class="sf-profiler-timeline">
<div id="legend-{{ token }}" class="legends"></div>
<svg id="timeline-{{ token }}" class="timeline-graph"></svg>
<script>{% autoescape 'js' %}
window.addEventListener('load', function onLoad() {
const theme = new Theme();
new TimelineEngine(
theme,
new SvgRenderer(document.getElementById('timeline-{{ token }}')),
new Legend(document.getElementById('legend-{{ token }}'), theme),
document.getElementById('threshold'),
{{ _self.dump_request_data(token, events, origin) }}
);
});
{% endautoescape %}</script>
</div>
{% endmacro %}

View File

@@ -0,0 +1,457 @@
'use strict';
class TimelineEngine {
/**
* @param {Theme} theme
* @param {Renderer} renderer
* @param {Legend} legend
* @param {Element} threshold
* @param {Object} request
* @param {Number} eventHeight
* @param {Number} horizontalMargin
*/
constructor(theme, renderer, legend, threshold, request, eventHeight = 36, horizontalMargin = 10) {
this.theme = theme;
this.renderer = renderer;
this.legend = legend;
this.threshold = threshold;
this.request = request;
this.scale = renderer.width / request.end;
this.eventHeight = eventHeight;
this.horizontalMargin = horizontalMargin;
this.labelY = Math.round(this.eventHeight * 0.48);
this.periodY = Math.round(this.eventHeight * 0.66);
this.FqcnMatcher = /\\([^\\]+)$/i;
this.origin = null;
this.createEventElements = this.createEventElements.bind(this);
this.createBackground = this.createBackground.bind(this);
this.createPeriod = this.createPeriod.bind(this);
this.render = this.render.bind(this);
this.renderEvent = this.renderEvent.bind(this);
this.renderPeriod = this.renderPeriod.bind(this);
this.onResize = this.onResize.bind(this);
this.isActive = this.isActive.bind(this);
this.threshold.addEventListener('change', this.render);
this.legend.addEventListener('change', this.render);
window.addEventListener('resize', this.onResize);
this.createElements();
this.render();
}
onResize() {
this.renderer.measure();
this.setScale(this.renderer.width / this.request.end);
}
setScale(scale) {
if (scale !== this.scale) {
this.scale = scale;
this.render();
}
}
createElements() {
this.origin = this.renderer.setFullVerticalLine(this.createBorder(), 0);
this.renderer.add(this.origin);
this.request.events
.filter(event => event.category === 'section')
.map(this.createBackground)
.forEach(this.renderer.add);
this.request.events
.map(this.createEventElements)
.forEach(this.renderer.add);
}
createBackground(event) {
const subrequest = event.name === '__section__.child';
const background = this.renderer.create('rect', subrequest ? 'timeline-subrequest' : 'timeline-border');
event.elements = Object.assign(event.elements || {}, { background });
return background;
}
createEventElements(event) {
const { name, category, duration, memory, periods } = event;
const border = this.renderer.setFullHorizontalLine(this.createBorder(), 0);
const lines = periods.map(period => this.createPeriod(period, category));
const label = this.createLabel(this.getShortName(name), duration, memory, periods[0]);
const title = this.renderer.createTitle(name);
const group = this.renderer.group([title, border, label].concat(lines), this.theme.getCategoryColor(event.category));
event.elements = Object.assign(event.elements || {}, { group, label, border });
this.legend.add(event.category)
return group;
}
createLabel(name, duration, memory, period) {
const label = this.renderer.createText(name, period.start * this.scale, this.labelY, 'timeline-label');
const sublabel = this.renderer.createTspan(` ${duration} ms / ${memory} MiB`, 'timeline-sublabel');
label.appendChild(sublabel);
return label;
}
createPeriod(period, category) {
const timeline = this.renderer.createPath(null, 'timeline-period', this.theme.getCategoryColor(category));
period.draw = category === 'section' ? this.renderer.setSectionLine : this.renderer.setPeriodLine;
period.elements = Object.assign(period.elements || {}, { timeline });
return timeline;
}
createBorder() {
return this.renderer.createPath(null, 'timeline-border');
}
isActive(event) {
const { duration, category } = event;
return duration >= this.threshold.value && this.legend.isActive(category);
}
render() {
const events = this.request.events.filter(this.isActive);
const width = this.renderer.width + this.horizontalMargin * 2;
const height = this.eventHeight * events.length;
// Set view box
this.renderer.setViewBox(-this.horizontalMargin, 0, width, height);
// Show 0ms origin
this.renderer.setFullVerticalLine(this.origin, 0);
// Render all events
this.request.events.forEach(event => this.renderEvent(event, events.indexOf(event)));
}
renderEvent(event, index) {
const { name, category, duration, memory, periods, elements } = event;
const { group, label, border, background } = elements;
const visible = index >= 0;
group.setAttribute('visibility', visible ? 'visible' : 'hidden');
if (background) {
background.setAttribute('visibility', visible ? 'visible' : 'hidden');
if (visible) {
const [min, max] = this.getEventLimits(event);
this.renderer.setFullRectangle(background, min * this.scale, max * this.scale);
}
}
if (visible) {
// Position the group
group.setAttribute('transform', `translate(0, ${index * this.eventHeight})`);
// Update top border
this.renderer.setFullHorizontalLine(border, 0);
// render label and ensure it doesn't escape the viewport
this.renderLabel(label, event);
// Update periods
periods.forEach(this.renderPeriod);
}
}
renderLabel(label, event) {
const width = this.getLabelWidth(label);
const [min, max] = this.getEventLimits(event);
const alignLeft = (min * this.scale) + width <= this.renderer.width;
label.setAttribute('x', (alignLeft ? min : max) * this.scale);
label.setAttribute('text-anchor', alignLeft ? 'start' : 'end');
}
renderPeriod(period) {
const { elements, start, duration } = period;
period.draw(elements.timeline, start * this.scale, this.periodY, Math.max(duration * this.scale, 1));
}
getLabelWidth(label) {
if (typeof label.width === 'undefined') {
label.width = label.getBBox().width;
}
return label.width;
}
getEventLimits(event) {
if (typeof event.limits === 'undefined') {
const { periods } = event;
event.limits = [
periods[0].start,
periods[periods.length - 1].end
];
}
return event.limits;
}
getShortName(name) {
const matches = this.FqcnMatcher.exec(name);
if (matches) {
return matches[1];
}
return name;
}
}
class Legend {
constructor(element, theme) {
this.element = element;
this.theme = theme;
this.toggle = this.toggle.bind(this);
this.createCategory = this.createCategory.bind(this);
this.categories = [];
this.theme.getDefaultCategories().forEach(this.createCategory);
}
add(category) {
this.get(category).classList.add('present');
}
createCategory(category) {
const element = document.createElement('button');
element.className = `timeline-category active`;
element.style.borderColor = this.theme.getCategoryColor(category);
element.innerText = category;
element.value = category;
element.type = 'button';
element.addEventListener('click', this.toggle);
this.element.appendChild(element);
this.categories.push(element);
return element;
}
toggle(event) {
event.target.classList.toggle('active');
this.emit('change');
}
isActive(category) {
return this.get(category).classList.contains('active');
}
get(category) {
return this.categories.find(element => element.value === category) || this.createCategory(category);
}
emit(name) {
this.element.dispatchEvent(new Event(name));
}
addEventListener(name, callback) {
this.element.addEventListener(name, callback);
}
removeEventListener(name, callback) {
this.element.removeEventListener(name, callback);
}
}
class SvgRenderer {
/**
* @param {SVGElement} element
*/
constructor(element) {
this.ns = 'http://www.w3.org/2000/svg';
this.width = null;
this.viewBox = {};
this.element = element;
this.add = this.add.bind(this);
this.setViewBox(0, 0, 0, 0);
this.measure();
}
setViewBox(x, y, width, height) {
this.viewBox = { x, y, width, height };
this.element.setAttribute('viewBox', `${x} ${y} ${width} ${height}`);
}
measure() {
this.width = this.element.getBoundingClientRect().width;
}
add(element) {
this.element.appendChild(element);
}
group(elements, className) {
const group = this.create('g', className);
elements.forEach(element => group.appendChild(element));
return group;
}
setHorizontalLine(element, x, y, width) {
element.setAttribute('d', `M${x},${y} h${width}`);
return element;
}
setVerticalLine(element, x, y, height) {
element.setAttribute('d', `M${x},${y} v${height}`);
return element;
}
setFullHorizontalLine(element, y) {
return this.setHorizontalLine(element, this.viewBox.x, y, this.viewBox.width);
}
setFullVerticalLine(element, x) {
return this.setVerticalLine(element, x, this.viewBox.y, this.viewBox.height);
}
setFullRectangle(element, min, max) {
element.setAttribute('x', min);
element.setAttribute('y', this.viewBox.y);
element.setAttribute('width', max - min);
element.setAttribute('height', this.viewBox.height);
}
setSectionLine(element, x, y, width, height = 4, markerSize = 6) {
const totalHeight = height + markerSize;
const maxMarkerWidth = Math.min(markerSize, width / 2);
const widthWithoutMarker = Math.max(0, width - (maxMarkerWidth * 2));
element.setAttribute('d', `M${x},${y + totalHeight} v${-totalHeight} h${width} v${totalHeight} l${-maxMarkerWidth} ${-markerSize} h${-widthWithoutMarker} Z`);
}
setPeriodLine(element, x, y, width, height = 4, markerWidth = 2, markerHeight = 4) {
const totalHeight = height + markerHeight;
const maxMarkerWidth = Math.min(markerWidth, width);
element.setAttribute('d', `M${x + maxMarkerWidth},${y + totalHeight} h${-maxMarkerWidth} v${-totalHeight} h${width} v${height} h${maxMarkerWidth-width}Z`);
}
createText(content, x, y, className) {
const element = this.create('text', className);
element.setAttribute('x', x);
element.setAttribute('y', y);
element.textContent = content;
return element;
}
createTspan(content, className) {
const element = this.create('tspan', className);
element.textContent = content;
return element;
}
createTitle(content) {
const element = this.create('title');
element.textContent = content;
return element;
}
createPath(path = null, className = null, color = null) {
const element = this.create('path', className);
if (path) {
element.setAttribute('d', path);
}
if (color) {
element.setAttribute('fill', color);
}
return element;
}
create(name, className = null) {
const element = document.createElementNS(this.ns, name);
if (className) {
element.setAttribute('class', className);
}
return element;
}
}
class Theme {
constructor(element) {
this.reservedCategoryColors = {
'default': '#737373',
'section': '#a3a3a3',
'event_listener': '#54aeff',
'template': '#4ac26b',
'doctrine': '#fd8c73',
'messenger_middleware': '#ff8182',
'controller.argument_value_resolver': '#c297ff',
'http_client': '#d4a72c',
};
this.customCategoryColors = [
'#d4a72c', // dark yellow
'#ffaba8', // light red
'#e6af05', // yellow
'#6fdd8b', // light green
'#76e3ea', // cyan
'#a475f9', // light purple
];
this.getCategoryColor = this.getCategoryColor.bind(this);
this.getDefaultCategories = this.getDefaultCategories.bind(this);
}
getDefaultCategories() {
return Object.keys(this.reservedCategoryColors);
}
getCategoryColor(category) {
return this.reservedCategoryColors[category] || this.getRandomColor(category);
}
getRandomColor(category) {
// instead of pure randomness, colors are assigned deterministically based on the
// category name, to ensure that each custom category always displays the same color
return this.customCategoryColors[this.hash(category) % this.customCategoryColors.length];
}
// copied from https://github.com/darkskyapp/string-hash
hash(string) {
var hash = 5381;
var i = string.length;
while(i) {
hash = (hash * 33) ^ string.charCodeAt(--i);
}
return hash >>> 0;
}
}

View File

@@ -0,0 +1,227 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% if collector.messages|length %}
{% set icon %}
{{ source('@WebProfiler/Icon/translation.svg') }}
{% set status_color = collector.countMissings ? 'red' : collector.countFallbacks ? 'yellow' %}
{% set error_count = collector.countMissings + collector.countFallbacks %}
<span class="sf-toolbar-value">{{ error_count ?: collector.countDefines }}</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Default locale</b>
<span class="sf-toolbar-status">
{{ collector.locale|default('-') }}
</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Missing messages</b>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.countMissings ? 'red' }}">
{{ collector.countMissings }}
</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Fallback messages</b>
<span class="sf-toolbar-status sf-toolbar-status-{{ collector.countFallbacks ? 'yellow' }}">
{{ collector.countFallbacks }}
</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Defined messages</b>
<span class="sf-toolbar-status">{{ collector.countDefines }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label label-status-{{ collector.countMissings ? 'error' : collector.countFallbacks ? 'warning' }} {{ collector.messages is empty ? 'disabled' }}">
<span class="icon">{{ source('@WebProfiler/Icon/translation.svg') }}</span>
<strong>Translation</strong>
{% if collector.countMissings or collector.countFallbacks %}
{% set error_count = collector.countMissings + collector.countFallbacks %}
<span class="count">
<span>{{ error_count }}</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
<h2>Translation</h2>
<div class="metrics">
<div class="metric">
<span class="value">{{ collector.locale|default('-') }}</span>
<span class="label">Default locale</span>
</div>
<div class="metric">
<span class="value">{{ collector.fallbackLocales|join(', ')|default('-') }}</span>
<span class="label">Fallback locale{{ collector.fallbackLocales|length != 1 ? 's' }}</span>
</div>
</div>
<h2>Messages</h2>
{% if collector.messages is empty %}
<div class="empty empty-panel">
<p>No translations have been called.</p>
</div>
{% else %}
{% block messages %}
{# sort translation messages in groups #}
{% set messages_defined, messages_missing, messages_fallback = [], [], [] %}
{% for message in collector.messages %}
{% if message.state == constant('Symfony\\Component\\Translation\\DataCollectorTranslator::MESSAGE_DEFINED') %}
{% set messages_defined = messages_defined|merge([message]) %}
{% elseif message.state == constant('Symfony\\Component\\Translation\\DataCollectorTranslator::MESSAGE_MISSING') %}
{% set messages_missing = messages_missing|merge([message]) %}
{% elseif message.state == constant('Symfony\\Component\\Translation\\DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK') %}
{% set messages_fallback = messages_fallback|merge([message]) %}
{% endif %}
{% endfor %}
<div class="sf-tabs">
<div class="tab {{ collector.countMissings == 0 ? 'active' }}">
<h3 class="tab-title">Defined <span class="badge">{{ collector.countDefines }}</span></h3>
<div class="tab-content">
<p class="help">
These messages are correctly translated into the given locale.
</p>
{% if messages_defined is empty %}
<div class="empty">
<p>None of the used translation messages are defined for the given locale.</p>
</div>
{% else %}
{% block defined_messages %}
{{ _self.render_table(messages_defined) }}
{% endblock %}
{% endif %}
</div>
</div>
<div class="tab">
<h3 class="tab-title">Fallback <span class="badge {{ collector.countFallbacks ? 'status-warning' }}">{{ collector.countFallbacks }}</span></h3>
<div class="tab-content">
<p class="help">
These messages are not available for the given locale
but Symfony found them in the fallback locale catalog.
</p>
{% if messages_fallback is empty %}
<div class="empty">
<p>No fallback translation messages were used.</p>
</div>
{% else %}
{% block fallback_messages %}
{{ _self.render_table(messages_fallback, true) }}
{% endblock %}
{% endif %}
</div>
</div>
<div class="tab {{ collector.countMissings > 0 ? 'active' }}">
<h3 class="tab-title">Missing <span class="badge {{ collector.countMissings ? 'status-error' }}">{{ collector.countMissings }}</span></h3>
<div class="tab-content">
<p class="help">
These messages are not available for the given locale and cannot
be found in the fallback locales. Add them to the translation
catalogue to avoid Symfony outputting untranslated contents.
</p>
{% if messages_missing is empty %}
<div class="empty">
<p>There are no messages of this category.</p>
</div>
{% else %}
{% block missing_messages %}
{{ _self.render_table(messages_missing) }}
{% endblock %}
{% endif %}
</div>
</div>
</div>
{% endblock messages %}
{% endif %}
{% if collector.globalParameters|default([]) %}
<h2>Global parameters</h2>
<table>
<thead>
<tr>
<th>Message ID</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for id, value in collector.globalParameters %}
<tr>
<td class="font-normal text-small nowrap">{{ id }}</td>
<td class="font-normal text-small nowrap">{{ profiler_dump(value) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endblock %}
{% macro render_table(messages, is_fallback) %}
<table>
<thead>
<tr>
<th>Locale</th>
{% if is_fallback %}
<th>Fallback locale</th>
{% endif %}
<th>Domain</th>
<th>Times used</th>
<th>Message ID</th>
<th>Message Preview</th>
</tr>
</thead>
<tbody>
{% for message in messages %}
<tr>
<td class="font-normal text-small nowrap">{{ message.locale }}</td>
{% if is_fallback %}
<td class="font-normal text-small nowrap">{{ message.fallbackLocale|default('-') }}</td>
{% endif %}
<td class="font-normal text-small text-bold nowrap">{{ message.domain }}</td>
<td class="font-normal text-small nowrap">{{ message.count }}</td>
<td>
<span class="{{ message.id|length < 64 ? 'nowrap' }}">{{ message.id }}</span>
{% if message.transChoiceNumber is not null %}
<small class="newline">(pluralization is used)</small>
{% endif %}
{% if message.parameters|length > 0 %}
<button class="btn-link newline text-small sf-toggle" data-toggle-selector="#parameters-{{ loop.index }}" data-toggle-alt-content="Hide parameters">Show parameters</button>
<div id="parameters-{{ loop.index }}" class="hidden">
{% for parameters in message.parameters %}
{{ profiler_dump(parameters, maxDepth=1) }}
{% endfor %}
</div>
{% endif %}
</td>
<td class="prewrap">{{ message.translation }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endmacro %}

View File

@@ -0,0 +1,168 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block head %}
{{ parent() }}
<style>
#twig-dump pre {
font-size: var(--font-size-monospace);
line-height: 1.7;
background-color: var(--page-background);
border: var(--border);
border-radius: 6px;
padding: 15px;
box-shadow: 0 0 1px rgba(128, 128, 128, .2);
}
#twig-dump span {
border-radius: 2px;
padding: 1px 2px;
}
#twig-dump .status-error { background: transparent; color: var(--color-error); }
#twig-dump .status-warning { background: rgba(240, 181, 24, 0.3); }
#twig-dump .status-success { background: rgba(100, 189, 99, 0.2); }
#twig-dump .status-info { background: var(--info-background); }
.theme-dark #twig-dump .status-warning { color: var(--yellow-200); }
.theme-dark #twig-dump .status-success { color: var(--green-200); }
#twig-table tbody td {
position: relative;
}
#twig-table tbody td div {
margin: 0;
}
#twig-table .template-file-path {
color: var(--color-muted);
display: block;
}
</style>
{% endblock %}
{% block toolbar %}
{% set time = collector.templatecount ? '%0.0f'|format(collector.time) : 'n/a' %}
{% set icon %}
{{ source('@WebProfiler/Icon/twig.svg') }}
<span class="sf-toolbar-value">{{ time }}</span>
<span class="sf-toolbar-label">ms</span>
{% endset %}
{% set text %}
{% set template = collector.templates|keys|first %}
{% set file = collector.templatePaths[template]|default(false) %}
{% set link = file ? file|file_link(1) : false %}
<div class="sf-toolbar-info-piece">
<b>Entry View</b>
<span>
{% if link %}
<a href="{{ link }}" title="{{ file }}">
{{ template }}
</a>
{% else %}
{{ template }}
{% endif %}
</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Render Time</b>
<span>{{ time }} ms</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Template Calls</b>
<span class="sf-toolbar-status">{{ collector.templatecount }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Block Calls</b>
<span class="sf-toolbar-status">{{ collector.blockcount }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Macro Calls</b>
<span class="sf-toolbar-status">{{ collector.macrocount }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }}
{% endblock %}
{% block menu %}
<span class="label {{ 0 == collector.templateCount ? 'disabled' }}">
<span class="icon">{{ source('@WebProfiler/Icon/twig.svg') }}</span>
<strong>Twig</strong>
</span>
{% endblock %}
{% block panel %}
{% if collector.templatecount == 0 %}
<h2>Twig</h2>
<div class="empty empty-panel">
<p>No Twig templates were rendered.</p>
</div>
{% else %}
<h2>Twig Metrics</h2>
<div class="metrics">
<div class="metric">
<span class="value">{{ '%0.0f'|format(collector.time) }} <span class="unit">ms</span></span>
<span class="label">Render time</span>
</div>
<div class="metric-divider"></div>
<div class="metric-group">
<div class="metric">
<span class="value">{{ collector.templatecount }}</span>
<span class="label">Template calls</span>
</div>
<div class="metric">
<span class="value">{{ collector.blockcount }}</span>
<span class="label">Block calls</span>
</div>
<div class="metric">
<span class="value">{{ collector.macrocount }}</span>
<span class="label">Macro calls</span>
</div>
</div>
</div>
<p class="help">
Render time includes sub-requests rendering time (if any).
</p>
<h2>Rendered Templates</h2>
<table id="twig-table">
<thead>
<tr>
<th scope="col">Template Name &amp; Path</th>
<th class="num-col" scope="col">Render Count</th>
</tr>
</thead>
<tbody>
{% for template, count in collector.templates %}
<tr>
{% set file = collector.templatePaths[template]|default(false) %}
{% set link = file ? file|file_link(1) : false %}
<td class="font-normal">
{% if link %}
<a href="{{ link }}" title="{{ file }}" class="stretched-link">
{{ template }}
<span class="template-file-path">{{ file|file_relative|default(file) }}</span>
</a>
{% else %}
{{ template }}
{% endif %}
</td>
<td class="font-normal num-col">{{ count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<h2>Rendering Call Graph</h2>
<div id="twig-dump">
{{ collector.htmlcallgraph }}
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,132 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block head %}
{{ parent() }}
<style>
#collector-content .sf-validator {
margin-bottom: 2em;
}
#collector-content .sf-validator .sf-validator-context,
#collector-content .sf-validator .trace {
border: var(--border);
background: var(--base-0);
padding: 10px;
margin: 0.5em 0;
overflow: auto;
}
#collector-content .sf-validator .trace {
font-size: 12px;
}
#collector-content .sf-validator .trace li {
margin-bottom: 0;
padding: 0;
}
#collector-content .sf-validator .trace li.selected {
background: var(--highlight-selected-line);
}
</style>
{% endblock %}
{% block toolbar %}
{% if collector.violationsCount > 0 or collector.calls|length %}
{% set status_color = collector.violationsCount ? 'red' %}
{% set icon %}
{{ source('@WebProfiler/Icon/validator.svg') }}
<span class="sf-toolbar-value">
{{ collector.violationsCount ?: collector.calls|length }}
</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Validator calls</b>
<span class="sf-toolbar-status">{{ collector.calls|length }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Number of violations</b>
<span class="sf-toolbar-status {{- collector.violationsCount > 0 ? ' sf-toolbar-status-red' }}">{{ collector.violationsCount }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label {{- collector.violationsCount ? ' label-status-error' }} {{ collector.calls is empty ? 'disabled' }}">
<span class="icon">{{ source('@WebProfiler/Icon/validator.svg') }}</span>
<strong>Validator</strong>
{% if collector.violationsCount > 0 %}
<span class="count">
<span>{{ collector.violationsCount }}</span>
</span>
{% endif %}
</span>
{% endblock %}
{% block panel %}
<h2>Validator calls</h2>
{% for call in collector.calls %}
<div class="sf-validator sf-reset">
<span class="metadata">In
{% set caller = call.caller %}
{% if caller.line %}
{% set link = caller.file|file_link(caller.line) %}
{% if link %}
<a href="{{ link }}" title="{{ caller.file }}">{{ caller.name }}</a>
{% else %}
<abbr title="{{ caller.file }}">{{ caller.name }}</abbr>
{% endif %}
{% else %}
{{ caller.name }}
{% endif %}
line <button type="button" class="btn-link text-small sf-toggle" data-toggle-selector="#sf-trace-{{ loop.index0 }}">{{ caller.line }}</button> (<button type="button" class="btn-link text-small sf-toggle" data-toggle-selector="#sf-context-{{ loop.index0 }}">context</button>):
</span>
<div class="sf-validator-compact hidden" id="sf-trace-{{ loop.index0 }}">
<div class="trace">
{{ caller.file|file_excerpt(caller.line)|replace({
'#DD0000': 'var(--highlight-string)',
'#007700': 'var(--highlight-keyword)',
'#0000BB': 'var(--highlight-default)',
'#FF8000': 'var(--highlight-comment)'
})|raw }}
</div>
</div>
<div class="sf-validator-compact hidden sf-validator-context" id="sf-context-{{ loop.index0 }}">
{{ profiler_dump(call.context, maxDepth=1) }}
</div>
{% if call.violations|length %}
<table>
<thead>
<tr>
<th>Path</th>
<th>Message</th>
<th>Invalid value</th>
<th>Violation</th>
</tr>
</thead>
{% for violation in call.violations %}
<tr>
<td>{{ violation.propertyPath }}</td>
<td>{{ violation.message }}</td>
<td>{{ profiler_dump(violation.seek('invalidValue')) }}</td>
<td>{{ profiler_dump(violation) }}</td>
</tr>
{% endfor %}
</table>
{% else %}
No violations
{% endif %}
</div>
{% else %}
<div class="empty empty-panel">
<p>No calls to the validator were collected.</p>
</div>
{% endfor %}
{% endblock %}

View File

@@ -0,0 +1,359 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block stylesheets %}
{{ parent() }}
<style>
:root {
--workflow-dialog-background: var(--gray-200);
}
.theme-dark {
--workflow-dialog-background: var(--gray-900);
}
dialog {
background: var(--workflow-dialog-background);
border: none;
border-radius: 6px;
box-shadow: var(--settings-modal-shadow);
max-width: 94%;
width: 1200px;
}
dialog::backdrop {
background: linear-gradient(
45deg,
rgb(18, 18, 20, 0.4),
rgb(17, 17, 20, 0.8)
);
}
dialog[open] {
animation: scale 0.3s ease normal;
}
dialog[open]::backdrop {
animation: backdrop 0.3s ease normal;
}
dialog.hide {
animation-direction: reverse;
}
dialog h2 {
box-shadow: none !important;
color: var(--page-color);
font-size: 18px;
margin: 0 0 .5em;
}
dialog i.cancel {
cursor: pointer;
padding: 0 5px;
float: right;
}
dialog table {
background: var(--page-background);
border-collapse: collapse;
border-radius: 4px;
box-shadow: inset 0 0 0 1px var(--table-border-color);
color: var(--page-color);
margin: 0 0 1em 0;
margin-bottom: 1em;
padding: 0;
}
dialog table tr {
border: 1px solid var(--table-border-color);
padding: .35em;
}
dialog table th,
dialog table td {
border: 1px solid var(--table-border-color);
padding: .625em;
text-align: left;
word-wrap: break-word;
}
dialog table thead th {
background: var(--table-header);
border-top-left-radius: 4px;
border-top-right-radius: 4px;
font-size: .85em;
}
dialog menu {
display: none;
}
@keyframes scale {
from { transform: scale(0); }
to { transform: scale(1); }
}
@keyframes backdrop {
from { opacity: 0; }
to { opacity: 1; }
}
</style>
{% endblock %}
{% block toolbar %}
{% if collector.callsCount > 0 %}
{% set icon %}
{{ source('@WebProfiler/Icon/workflow.svg') }}
<span class="sf-toolbar-value">{{ collector.callsCount }}</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Workflow Calls</b>
<span>{{ collector.callsCount }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label {{ collector.workflows|length == 0 ? 'disabled' }}">
<span class="icon">
{{ source('@WebProfiler/Icon/workflow.svg') }}
</span>
<strong>Workflow</strong>
</span>
{% endblock %}
{% block panel %}
<h2>Workflow</h2>
{% if collector.workflows|length == 0 %}
<div class="empty empty-panel">
<p>There are no workflows configured.</p>
</div>
{% else %}
<script>
{{ source('@WebProfiler/Script/Mermaid/mermaid-flowchart-v2.min.js') }}
const isDarkMode = document.querySelector('body').classList.contains('theme-dark');
mermaid.initialize({
flowchart: {
useMaxWidth: true,
},
securityLevel: 'loose',
theme: 'base',
themeVariables: {
darkMode: isDarkMode,
fontFamily: 'var(--font-family-system)',
fontSize: 'var(--font-size-body)',
// the properties below don't support CSS variables
primaryColor: isDarkMode ? 'lightsteelblue' : 'aliceblue',
primaryTextColor: isDarkMode ? '#000' : '#000',
primaryBorderColor: isDarkMode ? 'steelblue' : 'lightsteelblue',
lineColor: isDarkMode ? '#939393' : '#d4d4d4',
secondaryColor: isDarkMode ? 'lightyellow' : 'lightyellow',
tertiaryColor: isDarkMode ? 'lightSalmon' : 'lightSalmon',
}
});
{% for name, data in collector.workflows %}
window.showNodeDetails{{ collector.hash(name) }} = function (node) {
const map = {{ data.listeners|json_encode|raw }};
showNodeDetails(node, map);
};
{% endfor %}
const showNodeDetails = function (node, map) {
const dialog = document.getElementById('detailsDialog');
dialog.querySelector('tbody').innerHTML = '';
for (const [eventName, listeners] of Object.entries(map[node])) {
listeners.forEach(listener => {
const row = document.createElement('tr');
const eventNameCode = document.createElement('code');
eventNameCode.textContent = eventName;
const eventNameCell = document.createElement('td');
eventNameCell.appendChild(eventNameCode);
row.appendChild(eventNameCell);
const listenerDetailsCell = document.createElement('td');
row.appendChild(listenerDetailsCell);
let listenerDetails;
const listenerDetailsCode = document.createElement('code');
listenerDetailsCode.textContent = listener.title;
if (listener.file) {
const link = document.createElement('a');
link.href = listener.file;
link.appendChild(listenerDetailsCode);
listenerDetails = link;
} else {
listenerDetails = listenerDetailsCode;
}
listenerDetailsCell.appendChild(listenerDetails);
if (typeof listener.guardExpressions === 'object') {
listenerDetailsCell.appendChild(document.createElement('br'));
const guardExpressionsWrapper = document.createElement('span');
guardExpressionsWrapper.appendChild(document.createTextNode('guard expressions: '));
listener.guardExpressions.forEach((expression, index) => {
if (index > 0) {
guardExpressionsWrapper.appendChild(document.createTextNode(', '));
}
const expressionCode = document.createElement('code');
expressionCode.textContent = expression;
guardExpressionsWrapper.appendChild(expressionCode);
});
listenerDetailsCell.appendChild(guardExpressionsWrapper);
}
dialog.querySelector('tbody').appendChild(row);
});
};
if (dialog.dataset.processed) {
dialog.showModal();
return;
}
dialog.addEventListener('click', (e) => {
const rect = dialog.getBoundingClientRect();
const inDialog =
rect.top <= e.clientY &&
e.clientY <= rect.top + rect.height &&
rect.left <= e.clientX &&
e.clientX <= rect.left + rect.width;
!inDialog && dialog.close();
});
dialog.querySelectorAll('.cancel').forEach(elt => {
elt.addEventListener('click', () => dialog.close());
});
dialog.showModal();
dialog.dataset.processed = true;
};
// We do not load all mermaid diagrams at once, but only when the tab is opened
// This is because mermaid diagrams are in a tab, and cannot be renderer with a
// "good size" if they are not visible
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.tab').forEach((el) => {
const observer = new MutationObserver(() => {
if (!el.classList.contains('block')) {
return;
}
const mEl = el.querySelector('.sf-mermaid');
if (mEl.dataset.processed) {
return;
}
mermaid.run({
nodes: [mEl],
});
});
observer.observe(el, { attributeFilter: ['class'] });
});
});
</script>
<div class="sf-tabs js-tabs">
{% for name, data in collector.workflows %}
<div class="tab">
<h2 class="tab-title">{{ name }}{% if data.calls|length %} ({{ data.calls|length }}){% endif %}</h2>
<div class="tab-content">
<h3>Definition</h3>
<pre class="sf-mermaid">
{{ data.dump|raw }}
{% for nodeId, events in data.listeners %}
click {{ nodeId }} showNodeDetails{{ collector.hash(name) }}
{% endfor %}
</pre>
<a href="{{ collector.buildMermaidLiveLink(name) }}">View on mermaid.live</a>
<h3>Calls</h3>
<table>
<thead>
<tr>
<th>#</th>
<th>Call</th>
<th>Args</th>
<th>Return</th>
<th>Exception</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
{% for call in data.calls %}
<tr>
<td class="font-normal text-small text-muted nowrap">{{ loop.index }}</td>
<td>
<code>{{ call.method }}()</code>
{% if call.previousMarking ?? null %}
<hr />
Previous marking:
{{ profiler_dump(call.previousMarking) }}
{% endif %}
</td>
<td>
{{ profiler_dump(call.args) }}
</td>
<td>
{% if call.return is defined %}
{% if call.return is same as true %}
<code>true</code>
{% elseif call.return is same as false %}
<code>false</code>
{% else %}
{{ profiler_dump(call.return) }}
{% endif %}
{% endif %}
</td>
<td>
{% if call.exception is defined %}
{{ profiler_dump(call.exception) }}
{% endif %}
</td>
<td>
{{ '%0.2f ms'|format(call.duration) }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<dialog id="detailsDialog">
<h2>
Event listeners
<i class="cancel">×</i>
</h2>
<table>
<thead>
<tr>
<th>event</th>
<th>listener</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<menu>
<small><i>⌨</i> <kbd>esc</kbd></small>
<button class="btn btn-sm cancel">Close</button>
</menu>
</dialog>
{% endblock %}

View File

@@ -0,0 +1,26 @@
Icons are from "Tabler Icons" (https://github.com/tabler/tabler-icons), a set of
free MIT-licensed high-quality SVG icons.
-----
MIT License
Copyright (c) 2020-2022 Paweł Kuna
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-arrows-down-up" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<line x1="17" y1="3" x2="17" y2="21"></line>
<path d="M10 18l-3 3l-3 -3"></path>
<line x1="7" y1="21" x2="7" y2="3"></line>
<path d="M20 6l-3 -3l-3 3"></path>
</svg>

After

Width:  |  Height:  |  Size: 472 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-alert-circle" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<circle cx="12" cy="12" r="9"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-paperclip" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M15 7l-6.5 6.5a1.5 1.5 0 0 0 3 3l6.5 -6.5a3 3 0 0 0 -6 -6l-6.5 6.5a4.5 4.5 0 0 0 9 9l6.5 -6.5"></path>
</svg>

After

Width:  |  Height:  |  Size: 408 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-stack-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" role="img">
<title>Cache</title>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<polyline points="12 4 4 8 12 12 20 8 12 4"></polyline>
<polyline points="4 12 12 16 20 12"></polyline>
<polyline points="4 16 12 20 20 16"></polyline>
</svg>

After

Width:  |  Height:  |  Size: 490 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-chevron-down" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 9l6 6l6 -6"></path>
</svg>

After

Width:  |  Height:  |  Size: 331 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-x" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>

After

Width:  |  Height:  |  Size: 380 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-terminal-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M8 9l3 3l-3 3"></path>
<line x1="13" y1="15" x2="16" y2="15"></line>
<rect x="3" y="4" width="18" height="16" rx="2"></rect>
</svg>

After

Width:  |  Height:  |  Size: 437 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-adjustments" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" role="img">
<title>Config</title>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<circle cx="6" cy="10" r="2"></circle>
<line x1="6" y1="4" x2="6" y2="8"></line>
<line x1="6" y1="12" x2="6" y2="20"></line>
<circle cx="12" cy="16" r="2"></circle>
<line x1="12" y1="4" x2="12" y2="14"></line>
<line x1="12" y1="18" x2="12" y2="20"></line>
<circle cx="18" cy="7" r="2"></circle>
<line x1="18" y1="4" x2="18" y2="5"></line>
<line x1="18" y1="9" x2="18" y2="20"></line>
</svg>

After

Width:  |  Height:  |  Size: 751 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-download" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2"></path>
<polyline points="7 11 12 16 17 11"></polyline>
<line x1="12" y1="4" x2="12" y2="16"></line>
</svg>

After

Width:  |  Height:  |  Size: 457 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-access-point" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<line x1="12" y1="12" x2="12" y2="12.01"></line>
<path d="M14.828 9.172a4 4 0 0 1 0 5.656"></path>
<path d="M17.657 6.343a8 8 0 0 1 0 11.314"></path>
<path d="M9.168 14.828a4 4 0 0 1 0 -5.656"></path>
<path d="M6.337 17.657a8 8 0 0 1 0 -11.314"></path>
</svg>

After

Width:  |  Height:  |  Size: 568 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-ghost" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" role="img">
<title>Exception</title>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M5 11a7 7 0 0 1 14 0v7a1.78 1.78 0 0 1 -3.1 1.4a1.65 1.65 0 0 0 -2.6 0a1.65 1.65 0 0 1 -2.6 0a1.65 1.65 0 0 0 -2.6 0a1.78 1.78 0 0 1 -3.1 -1.4v-7"></path>
<line x1="10" y1="10" x2="10.01" y2="10"></line>
<line x1="14" y1="10" x2="14.01" y2="10"></line>
<path d="M10 14a3.5 3.5 0 0 0 4 0"></path>
</svg>

After

Width:  |  Height:  |  Size: 649 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-file" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"></path>
</svg>

After

Width:  |  Height:  |  Size: 426 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-filter" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M5.5 5h13a1 1 0 0 1 .5 1.5l-5 5.5l0 7l-4 -3l0 -4l-5 -5.5a1 1 0 0 1 .5 -1.5"></path>
</svg>

After

Width:  |  Height:  |  Size: 386 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-forms" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" role="img">
<title>Cache</title>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 3a3 3 0 0 0 -3 3v12a3 3 0 0 0 3 3"></path>
<path d="M6 3a3 3 0 0 1 3 3v12a3 3 0 0 1 -3 3"></path>
<path d="M13 7h7a1 1 0 0 1 1 1v8a1 1 0 0 1 -1 1h-7"></path>
<path d="M5 7h-1a1 1 0 0 0 -1 1v8a1 1 0 0 0 1 1h1"></path>
<path d="M17 12h.01"></path>
<path d="M13 12h.01"></path>
</svg>

After

Width:  |  Height:  |  Size: 636 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-arrow-narrow-right" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<line x1="5" y1="12" x2="19" y2="12"></line>
<line x1="15" y1="16" x2="19" y2="12"></line>
<line x1="15" y1="8" x2="19" y2="12"></line>
</svg>

After

Width:  |  Height:  |  Size: 449 B

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-affiliate" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M5.931 6.936l1.275 4.249m5.607 5.609l4.251 1.275"></path>
<path d="M11.683 12.317l5.759 -5.759"></path>
<circle cx="5.5" cy="5.5" r="1.5"></circle>
<circle cx="18.5" cy="5.5" r="1.5"></circle>
<circle cx="18.5" cy="18.5" r="1.5"></circle>
<circle cx="8.5" cy="15.5" r="4.5"></circle>
</svg>

After

Width:  |  Height:  |  Size: 609 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-file-alert" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" role="img">
<title>Logger</title>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"></path>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
<line x1="12" y1="11" x2="12" y2="14"></line>
</svg>

After

Width:  |  Height:  |  Size: 572 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-mail" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<rect x="3" y="5" width="18" height="14" rx="2"></rect>
<polyline points="3 7 12 13 21 7"></polyline>
</svg>

After

Width:  |  Height:  |  Size: 398 B

View File

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-cpu" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" role="img">
<title>Memory</title>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<rect x="5" y="5" width="14" height="14" rx="1"></rect>
<path d="M9 9h6v6h-6z"></path>
<path d="M3 10h2"></path>
<path d="M3 14h2"></path>
<path d="M10 3v2"></path>
<path d="M14 3v2"></path>
<path d="M21 10h-2"></path>
<path d="M21 14h-2"></path>
<path d="M14 21v-2"></path>
<path d="M10 21v-2"></path>
</svg>

After

Width:  |  Height:  |  Size: 666 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-menu-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" role="img">
<title>Menu</title>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<line x1="4" y1="6" x2="20" y2="6"></line>
<line x1="4" y1="12" x2="20" y2="12"></line>
<line x1="4" y1="18" x2="20" y2="18"></line>
</svg>

After

Width:  |  Height:  |  Size: 469 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-subtask" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<line x1="6" y1="9" x2="12" y2="9"></line>
<line x1="4" y1="5" x2="8" y2="5"></line>
<path d="M6 5v11a1 1 0 0 0 1 1h5"></path>
<rect x="12" y="7" width="8" height="4" rx="1"></rect>
<rect x="12" y="15" width="8" height="4" rx="1"></rect>
</svg>

After

Width:  |  Height:  |  Size: 548 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-x" width="24" height="24" viewBox="0 0 24 24" stroke-width="3" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>

After

Width:  |  Height:  |  Size: 378 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-bell-ringing" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M10 5a2 2 0 0 1 4 0a7 7 0 0 1 4 6v3a4 4 0 0 0 2 3h-16a4 4 0 0 0 2 -3v-3a7 7 0 0 1 4 -6"></path>
<path d="M9 17v1a3 3 0 0 0 6 0v-1"></path>
<path d="M21 6.727a11.05 11.05 0 0 0 -2.794 -3.727"></path>
<path d="M3 6.727a11.05 11.05 0 0 1 2.792 -3.727"></path>
</svg>

After

Width:  |  Height:  |  Size: 577 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-corner-down-right" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" role="img">
<title>Redirect</title>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 6v6a3 3 0 0 0 3 3h10l-4 -4m0 8l4 -4"></path>
</svg>

After

Width:  |  Height:  |  Size: 400 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-arrow-back-up" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M9 13l-4 -4l4 -4m-4 4h11a4 4 0 0 1 0 8h-1"></path>
</svg>

After

Width:  |  Height:  |  Size: 360 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-browser" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<rect x="4" y="4" width="16" height="16" rx="1"></rect>
<line x1="4" y1="8" x2="20" y2="8"></line>
<line x1="8" y1="4" x2="8" y2="8"></line>
</svg>

After

Width:  |  Height:  |  Size: 443 B

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-directions" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 21v-4"></path>
<path d="M12 13v-4"></path>
<path d="M12 5v-2"></path>
<path d="M10 21h4"></path>
<path d="M8 5v4h11l2 -2l-2 -2z"></path>
<path d="M14 13v4h-8l-2 -2l2 -2z"></path>
</svg>

After

Width:  |  Height:  |  Size: 509 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-search" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<circle cx="10" cy="10" r="7"></circle>
<line x1="21" y1="21" x2="15" y2="15"></line>
</svg>

After

Width:  |  Height:  |  Size: 383 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-arrows-right" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<line x1="21" y1="17" x2="3" y2="17"></line>
<path d="M18 4l3 3l-3 3"></path>
<path d="M18 20l3 -3l-3 -3"></path>
<line x1="21" y1="7" x2="3" y2="7"></line>
</svg>

After

Width:  |  Height:  |  Size: 458 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-moon" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" />
</svg>

After

Width:  |  Height:  |  Size: 377 B

View File

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-sun-high" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M14.828 14.828a4 4 0 1 0 -5.656 -5.656a4 4 0 0 0 5.656 5.656z" />
<path d="M6.343 17.657l-1.414 1.414" />
<path d="M6.343 6.343l-1.414 -1.414" />
<path d="M17.657 6.343l1.414 -1.414" />
<path d="M17.657 17.657l1.414 1.414" />
<path d="M4 12h-2" />
<path d="M12 4v-2" />
<path d="M20 12h2" />
<path d="M12 20v2" />
</svg>

After

Width:  |  Height:  |  Size: 644 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-device-laptop" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<line x1="3" y1="19" x2="21" y2="19" />
<rect x="5" y="6" width="14" height="10" rx="1" />
</svg>

After

Width:  |  Height:  |  Size: 389 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-autofit-width" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M4 12v-6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v6" />
<path d="M10 18h-7" />
<path d="M21 18h-7" />
<path d="M6 15l-3 3l3 3" />
<path d="M18 15l3 3l-3 3" />
</svg>

After

Width:  |  Height:  |  Size: 483 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-rectangle" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<rect x="3" y="5" width="18" height="14" rx="2" />
</svg>

After

Width:  |  Height:  |  Size: 349 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-settings" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>

After

Width:  |  Height:  |  Size: 881 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Symfony" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M12 .9C5.8.9.9 5.8.9 12a11 11 0 1 0 22.2 0A11 11 0 0 0 12 .9zm6.5 6c-.6 0-.9-.3-.9-.8 0-.2 0-.4.2-.6l.2-.4c0-.3-.5-.4-.6-.4-1.8.1-2.3 2.5-2.7 4.4l-.2 1c1 .2 1.8 0 2.2-.3.6-.4-.2-.7-.1-1.2.1-.3.5-.5.7-.6.5 0 .7.5.7.9 0 .7-1 1.8-3 1.8l-.6-.1-.6 2.4c-.4 1.6-.8 3.8-2.4 5.7-1.4 1.7-2.9 1.9-3.5 1.9-1.2 0-1.9-.6-2-1.5 0-.8.7-1.3 1.2-1.3.6 0 1.1.5 1.1 1s-.2.6-.4.6c-.1.1-.3.2-.3.4 0 .1.1.3.4.3.5 0 .8-.3 1.1-.5 1.2-.9 1.6-2.7 2.2-5.7l.1-.7.7-3.2c-.8-.6-1.3-1.4-2.4-1.7-.6-.1-1.1.1-1.5.5-.4.5-.2 1.1.2 1.5l.7.6c.7.8 1.2 1.6 1 2.5-.3 1.5-2 2.6-4 1.9-1.8-.6-2-1.8-1.8-2.5.2-.6.6-.7 1.1-.6.5.2.6.7.6 1.2l-.1.3c-.2.1-.3.3-.3.4-.1.4.4.6.7.7.7.3 1.6-.2 1.8-.8a1 1 0 0 0-.4-1.1l-.7-.8c-.4-.4-1.1-1.4-.7-2.6.1-.5.4-.9.7-1.3a4 4 0 0 1 2.8-.6c1.2.4 1.8 1.1 2.6 1.8.5-1.2 1-2.4 1.8-3.5.9-.9 1.9-1.6 3.1-1.7 1.3.2 2.2.7 2.2 1.6 0 .4-.2 1.1-.9 1.1z"/></svg>

After

Width:  |  Height:  |  Size: 982 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-chart-infographic" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" role="img">
<title>Time</title>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<circle cx="7" cy="7" r="4"></circle>
<path d="M7 3v4h4"></path>
<line x1="9" y1="17" x2="9" y2="21"></line>
<line x1="17" y1="14" x2="17" y2="21"></line>
<line x1="13" y1="13" x2="13" y2="21"></line>
<line x1="21" y1="12" x2="21" y2="21"></line>
</svg>

After

Width:  |  Height:  |  Size: 606 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-language-hiragana" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 5h7"></path>
<path d="M7 4c0 4.846 0 7 .5 8"></path>
<path d="M10 8.5c0 2.286 -2 4.5 -3.5 4.5s-2.5 -1.135 -2.5 -2c0 -2 1 -3 3 -3s5 .57 5 2.857c0 1.524 -.667 2.571 -2 3.143"></path>
<path d="M12 20l4 -9l4 9"></path>
<path d="M19.1 18h-6.2"></path>
</svg>

After

Width:  |  Height:  |  Size: 579 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-seeding" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" role="img">
<title>Twig</title>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 10a6 6 0 0 0 -6 -6h-3v2a6 6 0 0 0 6 6h3"></path>
<path d="M12 14a6 6 0 0 1 6 -6h3v1a6 6 0 0 1 -6 6h-3"></path>
<line x1="12" y1="20" x2="12" y2="10"></line>
</svg>

After

Width:  |  Height:  |  Size: 507 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-checklist" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M9.615 20h-2.615a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8"></path>
<path d="M14 19l2 2l4 -4"></path>
<path d="M9 8h4"></path>
<path d="M9 12h2"></path>
</svg>

After

Width:  |  Height:  |  Size: 481 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 18m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"></path>
<path d="M7 6m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"></path>
<path d="M17 6m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"></path>
<path d="M7 8v2a2 2 0 0 0 2 2h6a2 2 0 0 0 2 -2v-2"></path>
<path d="M12 12l0 4"></path>
</svg>

After

Width:  |  Height:  |  Size: 527 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" data-icon-name="icon-tabler-check" width="24" height="24" viewBox="0 0 24 24" stroke-width="3" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M5 12l5 5l10 -10"></path>
</svg>

After

Width:  |  Height:  |  Size: 325 B

View File

@@ -0,0 +1,50 @@
{% set status_code = profile.statuscode|default(0) %}
{% set interrupted = command_collector is same as false ? null : command_collector.interruptedBySignal %}
{% set css_class = status_code == 113 or interrupted is not null ? 'status-warning' : status_code > 0 ? 'status-error' : 'status-success' %}
<div class="terminal status {{ css_class }}">
<div class="container">
<h2>
<span class="status-request-method">
{{ profile.method|upper }}
</span>
<span class="status-command">
{{ profile.url }}
</span>
</h2>
<dl class="metadata">
{% if interrupted %}
<span class="status-response-status-code">Interrupted</span>
<dt>Signal</dt>
<dd class="status-response-status-text">{{ interrupted }}</dd>
<dt>Exit code</dt>
<dd class="status-response-status-text">{{ status_code }}</dd>
{% elseif status_code == 0 %}
<span class="status-response-status-code">Success</span>
{% elseif status_code > 0 %}
<span class="status-response-status-code">Error</span>
<dt>Exit code</dt>
<dd class="status-response-status-text"><span class="status-response-status-code">{{ status_code }}</span></dd>
{% endif %}
{% if request_collector.requestserver.has('SYMFONY_CLI_BINARY_NAME') %}
<dt>Symfony CLI</dt>
<dd>v{{ request_collector.requestserver.get('SYMFONY_CLI_VERSION') }}</dd>
{% endif %}
<dt>Application</dt>
<dd>
<a href="{{ path('_profiler_search_results', { token: token, limit: 10, ip: profile.ip, type: 'command' }) }}">{{ profile.ip }}</a>
</dd>
<dt>Profiled on</dt>
<dd><time data-convert-to-user-timezone data-render-as-datetime datetime="{{ profile.time|date('c') }}">{{ profile.time|date('r') }}</time></dd>
<dt>Token</dt>
<dd>{{ profile.token }}</dd>
</dl>
</div>
</div>

View File

@@ -0,0 +1,99 @@
{% set status_code = request_collector ? request_collector.statuscode|default(0) : 0 %}
{% set css_class = status_code > 399 ? 'status-error' : status_code > 299 ? 'status-warning' : 'status-success' %}
{% if request_collector and request_collector.redirect %}
{% set redirect = request_collector.redirect %}
{% set link_to_source_code = redirect.controller.class is defined ? redirect.controller.file|file_link(redirect.controller.line) %}
{% set redirect_route_name = '@' ~ redirect.route %}
<div class="status status-compact status-warning">
<span class="icon icon-redirect">{{ source('@WebProfiler/Icon/redirect.svg') }}</span>
<span class="status-response-status-code">{{ redirect.status_code }}</span> redirect from
<span class="status-request-method">{{ redirect.method }}</span>
{% if link_to_source_code %}
<a href="{{ link_to_source_code }}" title="{{ redirect.controller.file }}">{{ redirect_route_name }}</a>
{% else %}
{{ redirect_route_name }}
{% endif %}
(<a href="{{ path('_profiler', { token: redirect.token, panel: request.query.get('panel', 'request') }) }}">{{ redirect.token }}</a>)
</div>
{% endif %}
<div class="status {{ css_class }}">
{% if status_code > 399 %}
<p class="status-error-details">
<span class="icon">{{ source('@WebProfiler/Icon/alert-circle.svg') }}</span>
<span class="status-response-status-code">Error {{ status_code }}</span>
<span class="status-response-status-text">{{ request_collector.statusText }}</span>
</p>
{% endif %}
<h2>
<span class="status-request-method">
{{ profile.method|upper }}
</span>
{% set profile_title = profile.url|length < 160 ? profile.url : profile.url[:160] ~ '…' %}
{% if profile.method|upper in ['GET', 'HEAD'] %}
<a href="{{ profile.url }}">{{ profile_title }}</a>
{% else %}
{{ profile_title }}
{% endif %}
</h2>
<dl class="metadata">
{% if status_code < 400 %}
<dt>Response</dt>
<dd>
<span class="status-response-status-code">{{ status_code }}</span>
<span class="status-response-status-text">{{ request_collector.statusText }}</span>
</dd>
{% endif %}
{% set referer = request_collector ? request_collector.requestheaders.get('referer') : null %}
{% if referer %}
<dt></dt>
<dd>
<span class="icon icon-referer">{{ source('@WebProfiler/Icon/referrer.svg') }}</span>
<a href="{{ referer }}" class="referer">Browse referrer URL</a>
</dd>
{% endif %}
<dt>IP</dt>
<dd>
<a href="{{ path('_profiler_search_results', { token: token, limit: 10, ip: profile.ip }) }}">{{ profile.ip }}</a>
</dd>
<dt>Profiled on</dt>
<dd><time data-convert-to-user-timezone data-render-as-datetime datetime="{{ profile.time|date('c') }}">{{ profile.time|date('r') }}</time></dd>
<dt>Token</dt>
<dd>{{ profile.token }}</dd>
</dl>
</div>
{% if request_collector and request_collector.forwardtoken -%}
{% set forward_profile = profile.childByToken(request_collector.forwardtoken) %}
{% set controller = forward_profile ? forward_profile.collector('request').controller : 'n/a' %}
<div class="status status-compact status-compact-forward">
<span class="icon icon-forward">{{ source('@WebProfiler/Icon/forward.svg') }}</span>
Forwarded to
{% set link = controller.file is defined ? controller.file|file_link(controller.line) : null -%}
{%- if link %}<a href="{{ link }}" title="{{ controller.file }}">{% endif -%}
{% if controller.class is defined %}
{{- controller.class|abbr_class|striptags -}}
{{- controller.method ? ' :: ' ~ controller.method -}}
{% else %}
{{- controller -}}
{% endif %}
{%- if link %}</a>{% endif %}
(<a href="{{ path('_profiler', { token: request_collector.forwardtoken }) }}">{{ request_collector.forwardtoken }}</a>)
</div>
{%- endif %}

View File

@@ -0,0 +1 @@
{% block panel '' %}

View File

@@ -0,0 +1,22 @@
<div class="table-with-search-field">
<table class="{{ class|default('') }}">
<thead>
<tr>
<th scope="col" class="key">{{ labels is defined ? labels[0] : 'Key' }}</th>
<th scope="col">{{ labels is defined ? labels[1] : 'Value' }}</th>
</tr>
</thead>
<tbody>
{% for key in bag.keys|sort %}
<tr>
<th>{{ key }}</th>
<td>{{ profiler_dump(bag.get(key), maxDepth=maxDepth|default(0)) }}</td>
</tr>
{% else %}
<tr>
<td colspan="2">(no data)</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

View File

@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="{{ _charset }}" />
<meta name="robots" content="noindex,nofollow" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="view-transition" content="same-origin">
<title>{% block title %}Symfony Profiler{% endblock %}</title>
{% set request_collector = profile is defined ? profile.collectors.request|default(null) : null %}
{% set status_code = request_collector is not null ? request_collector.statuscode|default(0) : 0 %}
{% set favicon_color = status_code > 399 ? 'b41939' : status_code > 299 ? 'af8503' : '000000' %}
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg viewBox='0 0 600 600' xmlns='http://www.w3.org/2000/svg' fill-rule='evenodd' clip-rule='evenodd' stroke-linejoin='round' stroke-miterlimit='2'%3E%3Cstyle%3E%23circle %7B fill: %23{{ favicon_color }}; %7D %23sf %7B fill: %23ffffff; %7D%3C/style%3E%3Cpath fill='none' d='M0 0h600v600H0z'/%3E%3CclipPath id='a'%3E%3Cpath d='M0 0h600v600H0z'/%3E%3C/clipPath%3E%3Cg clip-path='url(%23a)'%3E%3Cpath id='circle' d='M599.985 299.974c0 165.696-134.307 300.024-300.003 300.024C134.302 599.998 0 465.67 0 299.974 0 134.304 134.302-.002 299.982-.002c165.696 0 300.003 134.307 300.003 299.976z' fill-rule='nonzero'/%3E%3Cpath id='sf' d='M431.154 110.993c-30.474 1.043-57.08 17.866-76.884 41.076-21.926 25.49-36.508 55.696-47.03 86.55-18.791-15.416-33.282-35.364-63.457-44.04-23.311-6.702-47.794-3.948-70.314 12.833-10.667 7.965-18.016 19.995-21.51 31.34-9.05 29.416 9.506 55.61 17.942 65.004l18.444 19.743c3.792 3.879 12.95 13.983 8.467 28.458-4.82 15.764-23.809 25.938-43.285 19.958-8.703-2.67-21.199-9.147-18.396-18.257 1.145-3.739 3.82-6.553 5.264-9.74 1.305-2.788 1.941-4.858 2.337-6.099 3.557-11.602-1.31-26.714-13.747-30.56-11.613-3.562-23.488-.738-28.094 14.202-5.22 16.979 2.905 47.795 46.436 61.206 51 15.694 94.13-12.084 100.249-48.287 3.857-22.675-6.392-39.536-25.147-61.2l-15.293-16.92c-9.254-9.248-12.437-25.018-2.856-37.134 8.093-10.233 19.6-14.581 38.476-9.457 27.543 7.468 39.809 26.58 60.285 41.996-8.44 27.741-13.977 55.584-18.973 80.548l-3.07 18.626c-14.636 76.766-25.816 118.939-54.856 143.144-5.858 4.167-14.218 10.399-26.821 10.843-6.622.203-8.757-4.355-8.847-6.344-.15-4.628 3.755-6.756 6.349-8.837 3.889-2.124 9.757-5.633 9.356-16.882-.423-13.293-11.431-24.815-27.35-24.286-11.919.402-30.09 11.608-29.4 32.149.701 21.22 20.472 37.118 50.288 36.107 15.935-.535 51.528-7.018 86.592-48.699 40.82-47.8 52.235-102.576 60.82-142.673l9.591-52.946a177.574 177.574 0 0017.209 1.22c50.844 1.075 76.257-25.249 76.653-44.41.257-11.591-7.6-23.011-18.61-22.739-7.863.22-17.759 5.473-20.123 16.353-2.332 10.671 16.17 20.316 1.712 29.704-10.27 6.643-28.683 11.319-54.615 7.526l4.712-26.061c9.623-49.416 21.493-110.193 66.528-111.68 3.284-.155 15.282.139 15.56 8.088.08 2.637-.582 3.332-3.68 9.393-3.166 4.729-4.36 8.773-4.204 13.394.433 12.608 10.024 20.91 23.916 20.429 18.572-.626 23.906-18.7 23.6-27.998-.759-21.846-23.776-35.647-54.224-34.641z' fill-rule='nonzero'/%3E%3C/g%3E%3C/svg%3E"/>
{% block head %}
{% block stylesheets %}
<style{% if csp_style_nonce is defined and csp_style_nonce %} nonce="{{ csp_style_nonce }}"{% endif %}>
{{ include('@WebProfiler/Profiler/profiler.css.twig') }}
</style>
{% endblock %}
{% block javascripts %}
{% endblock %}
{% endblock %}
</head>
<body>
<script{% if csp_script_nonce is defined and csp_script_nonce %} nonce="{{ csp_script_nonce }}"{% endif %}>
if (null === localStorage.getItem('symfony/profiler/theme') || 'theme-auto' === localStorage.getItem('symfony/profiler/theme')) {
document.body.classList.add((matchMedia('(prefers-color-scheme: dark)').matches ? 'theme-dark' : 'theme-light'));
// needed to respond dynamically to OS changes without having to refresh the page
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
document.body.classList.remove('theme-light', 'theme-dark');
document.body.classList.add(e.matches ? 'theme-dark' : 'theme-light');
});
} else {
document.body.classList.add(localStorage.getItem('symfony/profiler/theme'));
}
document.body.classList.add(localStorage.getItem('symfony/profiler/width') || 'width-normal');
document.body.classList.add(
(navigator.appVersion.indexOf('Win') !== -1) ? 'windows' : (navigator.appVersion.indexOf('Mac') !== -1) ? 'macos' : 'linux'
);
</script>
{% block body '' %}
</body>
</html>

View File

@@ -0,0 +1,280 @@
{# This file is partially duplicated in src/Symfony/Component/ErrorHandler/Resources/assets/js/exception.js.
If you make any change in this file, verify the same change is needed in the other file. #}
{# CAUTION: the contents of this file are processed by Twig before loading
them as JavaScript source code. Always use '/*' comments instead
of '//' comments to avoid impossible-to-debug side-effects #}
<script{% if csp_script_nonce is defined and csp_script_nonce %} nonce="{{ csp_script_nonce }}"{% endif %}>
window.addEventListener('DOMContentLoaded', () => {
new SymfonyProfiler();
});
class SymfonyProfiler {
constructor() {
this.#createTabs();
this.#createTableSearchFields();
this.#createToggles();
this.#createCopyToClipboard();
this.#convertDateTimesToUserTimezone();
}
#createTabs() {
/* the accessibility options of this component have been defined according to: */
/* www.w3.org/WAI/ARIA/apg/example-index/tabs/tabs-manual.html */
const tabGroups = document.querySelectorAll('.sf-tabs:not([data-processed=true])');
/* create the tab navigation for each group of tabs */
tabGroups.forEach((tabGroup, i) => {
const tabs = tabGroup.querySelectorAll(':scope > .tab');
const tabNavigation = document.createElement('div');
tabNavigation.classList.add('tab-navigation');
tabNavigation.setAttribute('role', 'tablist');
let selectedTabId = `tab-${i}-0`; /* select the first tab by default */
tabs.forEach((tab, j) => {
const tabId = `tab-${i}-${j}`;
const tabTitle = tab.querySelector('.tab-title').innerHTML;
const tabNavigationItem = document.createElement('button');
tabNavigationItem.classList.add('tab-control');
tabNavigationItem.setAttribute('data-tab-id', tabId);
tabNavigationItem.setAttribute('role', 'tab');
tabNavigationItem.setAttribute('aria-controls', tabId);
if (tab.classList.contains('active')) { selectedTabId = tabId; }
if (tab.classList.contains('disabled')) {
tabNavigationItem.classList.add('disabled');
}
tabNavigationItem.innerHTML = tabTitle;
tabNavigation.appendChild(tabNavigationItem);
const tabContent = tab.querySelector('.tab-content');
tabContent.parentElement.setAttribute('id', tabId);
});
tabGroup.insertBefore(tabNavigation, tabGroup.firstChild);
document.querySelector('[data-tab-id="' + selectedTabId + '"]').classList.add('active');
});
/* display the active tab and add the 'click' event listeners */
tabGroups.forEach((tabGroup) => {
const tabs = tabGroup.querySelectorAll(':scope > .tab-navigation .tab-control');
tabs.forEach((tab) => {
const tabId = tab.getAttribute('data-tab-id');
const tabPanel = document.getElementById(tabId);
tabPanel.setAttribute('role', 'tabpanel');
tabPanel.setAttribute('aria-labelledby', tabId);
tabPanel.querySelector('.tab-title').className = 'hidden';
if (tab.classList.contains('active')) {
tabPanel.className = 'block';
tab.setAttribute('aria-selected', 'true');
tab.removeAttribute('tabindex');
} else {
tabPanel.className = 'hidden';
tab.removeAttribute('aria-selected');
tab.setAttribute('tabindex', '-1');
}
tab.addEventListener('click', function(e) {
let activeTab = e.target || e.srcElement;
/* needed because when the tab contains HTML contents, user can click */
/* on any of those elements instead of their parent '<button>' element */
while ('button' !== activeTab.tagName.toLowerCase()) {
activeTab = activeTab.parentNode;
}
/* get the full list of tabs through the parent of the active tab element */
const tabs = Array.from(activeTab.parentNode.children);
tabs.forEach((tab) => {
const tabId = tab.getAttribute('data-tab-id');
document.getElementById(tabId).className = 'hidden';
tab.classList.remove('active');
tab.removeAttribute('aria-selected');
tab.setAttribute('tabindex', '-1');
});
activeTab.classList.add('active');
activeTab.setAttribute('aria-selected', 'true');
activeTab.removeAttribute('tabindex');
const activeTabId = activeTab.getAttribute('data-tab-id');
document.getElementById(activeTabId).className = 'block';
});
});
tabGroup.setAttribute('data-processed', 'true');
});
}
#createTableSearchFields() {
document.querySelectorAll('div.table-with-search-field').forEach((tableWrapper, i) => {
const searchField = document.createElement('input');
searchField.type = 'search';
searchField.placeholder = 'search...';
searchField.id = `table-search-field-${i}`;
searchField.classList.add(`table-search-field-input`);
searchField.autocapitalize = 'off';
searchField.autocomplete = 'off';
searchField.autocorrect = 'off';
tableWrapper.insertBefore(searchField, tableWrapper.firstChild);
const labelField = document.createElement('label');
labelField.htmlFor = `table-search-field-${i}`;
labelField.classList.add(`table-search-field-label`);
labelField.textContent = 'Search inside the contents of the table';
tableWrapper.insertBefore(labelField, tableWrapper.firstChild);
searchField.addEventListener('input', () => {
const query = searchField.value.toLowerCase();
let allRowsAreHidden = true;
tableWrapper.querySelectorAll('tbody tr').forEach((row) => {
const rowMatchesQuery = row.textContent.toLowerCase().includes(query);
row.style.display = rowMatchesQuery ? '' : 'none';
if (rowMatchesQuery) {
allRowsAreHidden = false;
}
});
/* if there are no results and all rows are hidden, show a message to avoid confusion */
const noResultsElement = tableWrapper.querySelector('.no-results-message');
if (allRowsAreHidden) {
if (null === noResultsElement) {
const noResultsElement = document.createElement('p');
noResultsElement.textContent = 'No results found.';
noResultsElement.classList.add('no-results-message');
tableWrapper.appendChild(noResultsElement);
} else {
noResultsElement.style.display = '';
}
} else {
if (null !== noResultsElement) {
noResultsElement.style.display = 'none';
}
}
});
});
}
#createToggles() {
const toggles = document.querySelectorAll('.sf-toggle:not([data-processed=true])');
toggles.forEach((toggle) => {
const elementSelector = toggle.getAttribute('data-toggle-selector');
const element = document.querySelector(elementSelector);
element.classList.add('sf-toggle-content');
if (toggle.hasAttribute('data-toggle-initial') && 'display' === toggle.getAttribute('data-toggle-initial')) {
toggle.classList.add('sf-toggle-on');
element.classList.add('sf-toggle-visible');
} else {
toggle.classList.add('sf-toggle-off');
element.classList.add('sf-toggle-hidden');
}
toggle.addEventListener('click', (e) => {
const toggle = e.currentTarget;
if (e.target.closest('a, .sf-toggle') !== toggle) {
return;
}
e.preventDefault();
if ('' !== window.getSelection().toString()) {
/* Don't do anything on text selection */
return;
}
const element = document.querySelector(toggle.getAttribute('data-toggle-selector'));
toggle.classList.toggle('sf-toggle-on');
toggle.classList.toggle('sf-toggle-off');
element.classList.toggle('sf-toggle-hidden');
element.classList.toggle('sf-toggle-visible');
/* the toggle doesn't change its contents when clicking on it */
if (!toggle.hasAttribute('data-toggle-alt-content')) {
return;
}
if (!toggle.hasAttribute('data-toggle-original-content')) {
toggle.setAttribute('data-toggle-original-content', toggle.innerHTML);
}
const currentContent = toggle.innerHTML;
const originalContent = toggle.getAttribute('data-toggle-original-content');
const altContent = toggle.getAttribute('data-toggle-alt-content');
toggle.innerHTML = currentContent !== altContent ? altContent : originalContent;
});
toggle.setAttribute('data-processed', 'true');
});
}
#createCopyToClipboard() {
if (!navigator.clipboard) {
return;
}
const copyToClipboardElements = document.querySelectorAll('[data-clipboard-text]');
copyToClipboardElements.forEach((copyToClipboardElement) => {
copyToClipboardElement.classList.remove('hidden');
copyToClipboardElement.addEventListener('click', (e) => {
/* Prevents from disallowing clicks on "copy to clipboard" elements inside toggles */
e.stopPropagation();
navigator.clipboard.writeText(copyToClipboardElement.getAttribute('data-clipboard-text'));
let oldContent = copyToClipboardElement.textContent;
copyToClipboardElement.textContent = `✅ Copied!`;
copyToClipboardElement.disabled = true;
setTimeout(() => {
copyToClipboardElement.textContent = oldContent;
copyToClipboardElement.disabled = false;
}, 7000);
});
});
}
#convertDateTimesToUserTimezone() {
const userTimezoneName = Intl.DateTimeFormat().resolvedOptions().timeZone;
document.querySelectorAll('time[data-convert-to-user-timezone]').forEach((timeElement) => {
const iso8601Datetime = timeElement.getAttribute('datetime');
const dateInUserTimezone = new Date(iso8601Datetime);
let options = {};
if (timeElement.hasAttribute('data-render-as-datetime')) {
options = {
year: 'numeric', month: 'long', day: 'numeric',
hour: 'numeric', minute: 'numeric', second: 'numeric'
};
} else if (timeElement.hasAttribute('data-render-as-date')) {
options = { year: 'numeric', month: 'long', day: 'numeric' };
} else if (timeElement.hasAttribute('data-render-as-time')) {
options = { hour: 'numeric', minute: 'numeric', second: 'numeric' };
}
if (timeElement.hasAttribute('data-render-with-millisecond-precision')) {
options.fractionalSecondDigits = 3;
}
/* dates/times are always rendered in English to match the rest of the Profiler interface */
timeElement.textContent = dateInUserTimezone.toLocaleString('en', options);
if (undefined !== userTimezoneName) {
const existingTitle = timeElement.getAttribute('title');
const newTitle = null === existingTitle
? `Date/times shown in your timezone: ${userTimezoneName}`
: existingTitle + ` (date/times shown in your timezone: ${userTimezoneName})`;
timeElement.setAttribute('title', newTitle);
}
});
}
}
</script>

View File

@@ -0,0 +1,25 @@
{% block toolbar %}
{% set icon %}
{{ source('@WebProfiler/Icon/symfony.svg') }}
<span class="sf-toolbar-value sf-toolbar-ajax-request-counter">
Loading&hellip;
</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Loading the web debug toolbar&hellip;</b>
</div>
<div class="sf-toolbar-info-piece">
Attempt #<span id="sfLoadCounter-{{ token }}"></span>
</div>
<div class="sf-toolbar-info-piece">
<b>
<button class="sf-cancel-button" type="button" id="sfLoadCancel-{{ token }}" title="Cancel loading">Cancel</button>
</b>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }}
{% endblock %}

View File

@@ -0,0 +1,12 @@
<div id="header">
<h1><a href="{{ path('_profiler_home') }}">{{ source('@WebProfiler/Icon/symfony.svg') }} Symfony Profiler</a></h1>
<div class="search">
<form method="get" action="https://symfony.com/search" target="_blank">
<div class="form-row">
<input name="q" id="search-id" type="search" placeholder="search on symfony.com" aria-label="Search on symfony.com">
<button type="submit" class="visually-hidden">Search</button>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,22 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% set messages = {
'no_token' : {
status: 'error',
title: (token|default('') == 'latest') ? 'There are no profiles' : 'Token not found',
message: (token|default('') == 'latest') ? 'No profiles found.' : 'Token "' ~ token|default('') ~ '" not found.'
}
} %}
{% block summary %}
<div class="status status-{{ messages[about].status }}">
<div class="container">
<h2>{{ messages[about].status|title }}</h2>
</div>
</div>
{% endblock %}
{% block panel %}
<h2>{{ messages[about].title }}</h2>
<p>{{ messages[about].message }}</p>
{% endblock %}

View File

@@ -0,0 +1,76 @@
{% extends '@WebProfiler/Profiler/base.html.twig' %}
{% block body %}
<div class="container">
{{ include('@WebProfiler/Profiler/header.html.twig', {profile_type: profile_type}, with_context = false) }}
<div id="summary">
{% block summary %}
{% if profile is defined %}
{% set request_collector = profile.collectors.request|default(false) %}
{{ include('@WebProfiler/Profiler/_%s_summary.html.twig'|format(profile_type), {
profile: profile,
command_collector: profile.collectors.command|default(false) ,
request_collector: request_collector,
request: request,
token: token
}, with_context=false) }}
{% endif %}
{% endblock %}
</div>
<div id="content">
<div id="main">
<div id="sidebar">
{% block sidebar %}
<div id="sidebar-contents">
<div id="sidebar-shortcuts">
{% block sidebar_shortcuts_links %}
<div class="shortcuts">
<a class="btn btn-link" href="{{ path('_profiler_search', { limit: 10, type: profile_type }) }}">{{ source('@WebProfiler/Icon/search.svg') }} Search profiles</a>
<a class="btn btn-link" href="{{ path('_profiler', { token: 'latest', type: profile_type }|merge(request.query.all)) }}">Latest</a>
</div>
{% endblock sidebar_shortcuts_links %}
</div>
{% if templates is defined %}
<ul id="menu-profiler">
{% if 'request' is same as(profile_type) %}
{% set excludes = ['command'] %}
{% elseif 'command' is same as(profile_type) %}
{% set excludes = ['request', 'router'] %}
{% endif %}
{% for name, template in templates|filter((t, n) => n not in excludes) %}
{% set menu -%}
{%- if block('menu', template) is defined -%}
{% with { collector: profile.getcollector(name), profiler_markup_version: profiler_markup_version } %}
{{- block('menu', template) -}}
{% endwith %}
{%- endif -%}
{%- endset %}
{% if menu is not empty %}
<li class="{{ name }} {{ name == panel ? 'selected' }}">
<a href="{{ path('_profiler', { token: token, panel: name, type: profile_type }) }}">{{ menu|raw }}</a>
</li>
{% endif %}
{% endfor %}
</ul>
{% endif %}
</div>
{{ include('@WebProfiler/Profiler/settings.html.twig') }}
{% endblock sidebar %}
</div>
<div id="collector-wrapper">
<div id="collector-content">
{{ include('@WebProfiler/Profiler/base_js.html.twig') }}
{% block panel '' %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,80 @@
#header {
margin-bottom: 30px;
}
#source {
background: var(--page-background);
border: 1px solid var(--menu-border-color);
box-shadow: 0 0 0 5px var(--page-background);
border-radius: 6px;
margin: 0 30px 45px 0;
max-width: 960px;
padding: 15px 20px;
}
.width-full #source {
max-width: unset;
width: 100%;
}
#source code {
font-size: 15px;
}
#source .source-file-name {
border-bottom: 1px solid var(--table-border-color);
font-size: 18px;
font-weight: 500;
margin: 0 0 15px 0;
padding: 0 0 15px;
}
#source .source-file-name small {
color: var(--color-muted);
}
#source .source-content {
overflow-x: auto;
}
#source .source-content ol {
margin: 0;
}
#source .source-content ol li {
margin: 0 0 2px 0;
padding-left: 5px;
white-space: preserve nowrap;
}
#source .source-content ol li::marker {
color: var(--color-muted);
font-family: var(--font-family-monospace);
padding-right: 5px;
}
#source .source-content li.selected {
background: var(--yellow-100);
border-radius: 4px;
}
.theme-dark #source .source-content li.selected {
background: var(--gray-600);
}
#source .source-content li.selected::marker {
color: var(--color-text);
font-weight: bold;
}
#source span[style="color: #FF8000"] { color: var(--highlight-comment) !important; }
#source span[style="color: #007700"] { color: var(--highlight-keyword) !important; }
#source span[style="color: #0000BB"] { color: var(--color-text) !important; }
#source span[style="color: #DD0000"] { color: var(--highlight-string) !important; }
.file-metadata dt {
color: var(--header-metadata-key);
display: block;
font-weight: bold;
}
.file-metadata dd {
color: var(--header-metadata-value);
margin: 5px 0 20px;
/* needed to break the long file paths */
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-all;
}

View File

@@ -0,0 +1,64 @@
{% extends '@WebProfiler/Profiler/base.html.twig' %}
{% block head %}
<style>
{{ include('@WebProfiler/Profiler/profiler.css.twig') }}
{{ include('@WebProfiler/Profiler/open.css.twig') }}
</style>
{% endblock %}
{% block body %}
<div class="container">
{{ include('@WebProfiler/Profiler/header.html.twig', with_context = false) }}
{% set source = file_info.pathname|file_excerpt(line, -1) %}
<div id="content">
<div id="main">
<div id="source">
<h1 class="source-file-name">{{ file }}{% if 0 < line %} <small>line {{ line }}</small>{% endif %}</h1>
<div class="source-content">
{% if source is null %}
<p class="empty empty-panel">The file is not readable.</p>
{% else %}
{{ source|raw }}
{% endif %}
</div>
</div>
<div id="sidebar">
<dl class="file-metadata">
<dt>Filepath:</dt>
<dd>{{ file_info.pathname }}</dd>
<dt>Last modified:</dt>
<dd>{{ file_info.mTime|date }}</dd>
<dt>Size:</dt>
{% set file_size_in_kb = file_info.size / 1024 %}
{% set file_num_lines = source|split("\n")|length - 1 %}
<dd>
{{ file_size_in_kb < 1 ? file_info.size ~ ' bytes' : file_size_in_kb|number_format(0) ~ ' KB' }}
/ {{ file_num_lines }} lines
</dd>
</dl>
<a class="doc-link" href="https://symfony.com/doc/{{ constant('Symfony\\Component\\HttpKernel\\Kernel::VERSION') }}/reference/configuration/framework.html#ide" rel="help">Open this file in your IDE?</a>
</div>
</div>
</div>
</div>
<script>
window.addEventListener('load', function () {
const selectedLineElement = document.querySelector('.source-content li.selected');
if (null === selectedLineElement) {
return;
}
const selectedLineYCoordinate = selectedLineElement.getBoundingClientRect().y;
console.log(selectedLineYCoordinate);
window.scrollTo({ top: selectedLineYCoordinate - 20, left: 0, behavior: 'smooth' });
});
</script>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,138 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% macro profile_search_filter(request, result, property) %}
{%- if request.hasSession -%}
<a href="{{ path('_profiler_search_results', request.query.all|merge({token: result.token})|merge({ (property): result[property] })) }}" title="Search"><span title="Search" class="sf-icon sf-search">{{ source('@WebProfiler/Icon/search.svg') }}</span></a>
{%- endif -%}
{% endmacro %}
{% block head %}
{{ parent() }}
<style>
#search-results td {
font-family: var(--font-family-system);
vertical-align: middle;
}
#search-results .sf-search {
visibility: hidden;
margin-left: 2px;
}
#search-results tr:hover .sf-search {
visibility: visible;
}
#search-results .sf-search svg {
stroke-width: 3;
}
</style>
{% endblock %}
{% block summary %}
<div class="status">
<h2>Profile Search</h2>
</div>
{% endblock %}
{% block sidebar_search_css_class %}{% endblock %}
{% block sidebar_shortcuts_links %}
{{ parent() }}
{{ render(controller('web_profiler.controller.profiler::searchBarAction', query={type: profile_type }|merge(request.query.all))) }}
{% endblock %}
{% block panel %}
<div class="sf-tabs" data-processed="true">
<div class="tab-navigation" role="tablist">
<button class="tab-control {{ 'request' == profile_type ? 'active' }}" role="tab" {{ 'request' == profile_type ? 'aria-selected="true"' : 'tabindex="-1"' }} >
<a href="{{ path('_profiler_search_results', {token: 'empty', limit: 10}) }}">
HTTP Requests
</a>
</button>
<button class="tab-control {{ 'command' == profile_type ? 'active' }}" role="tab" {{ 'command' == profile_type ? 'aria-selected="true"' : 'tabindex="-1"' }}>
<a href="{{ path('_profiler_search_results', {token: 'empty', limit: 10, type: 'command'}) }}">
Console Commands
</a>
</button>
</div>
</div>
<h2>{{ tokens ? tokens|length : 'No' }} results found</h2>
{% if tokens %}
<table id="search-results">
<thead>
<tr>
<th scope="col" class="text-center">
{% if 'command' == profile_type %}
Exit code
{% else %}
Status
{% endif %}
</th>
<th scope="col">
{% if 'command' == profile_type %}
Application
{% else %}
IP
{% endif %}
</th>
<th scope="col">
{% if 'command' == profile_type %}
Mode
{% else %}
Method
{% endif %}
</th>
<th scope="col">
{% if 'command' == profile_type %}
Command
{% else %}
URL
{% endif %}
</th>
<th scope="col">Time</th>
<th scope="col">Token</th>
</tr>
</thead>
<tbody>
{% for result in tokens %}
{% if 'command' == profile_type %}
{% set css_class = result.status_code == 113 ? 'status-warning' : result.status_code > 0 ? 'status-error' : 'status-success' %}
{% else %}
{% set css_class = result.status_code|default(0) > 399 ? 'status-error' : result.status_code|default(0) > 299 ? 'status-warning' : 'status-success' %}
{% endif %}
<tr>
<td class="text-center">
<span class="label {{ css_class }}">{{ result.status_code|default('n/a') }}</span>
</td>
<td>
<span class="nowrap">{{ result.ip }} {{ _self.profile_search_filter(request, result, 'ip') }}</span>
</td>
<td>
<span class="nowrap">{{ result.method }} {{ _self.profile_search_filter(request, result, 'method') }}</span>
</td>
<td class="break-long-words">
{{ result.url }}
{{ _self.profile_search_filter(request, result, 'url') }}
</td>
<td class="text-small">
<time data-convert-to-user-timezone data-render-as-date datetime="{{ result.time|date('c') }}">
{{ result.time|date('d-M-Y') }}
</time>
<time class="newline" data-convert-to-user-timezone data-render-as-time datetime="{{ result.time|date('c') }}">
{{ result.time|date('H:i:s') }}
</time>
</td>
<td class="nowrap"><a href="{{ path('_profiler', { token: result.token }) }}">{{ result.token }}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty empty-panel">
<p>The query returned no result.</p>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,93 @@
<div id="sidebar-search">
<form action="{{ path('_profiler_search') }}" method="get">
<div class="form-group">
<label for="ip">
{% if 'command' == profile_type %}
Application
{% else %}
IP
{% endif %}
</label>
<input type="text" name="ip" id="ip" value="{{ ip }}">
</div>
<div class="form-group-row">
<div class="form-group">
<label for="method">
{% if 'command' == profile_type %}
Mode
{% else %}
Method
{% endif %}
</label>
<select name="method" id="method">
<option value="">Any</option>
{% if 'command' == profile_type %}
{% set methods = ['BATCH', 'INTERACTIVE'] %}
{% else %}
{% set methods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT'] %}
{% endif %}
{% for m in methods %}
<option {{ m == method ? 'selected="selected"' }}>{{ m }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="status_code">
{% if 'command' == profile_type %}
Exit code
{% set min_and_max = 'min=%d max=%d'|format(0, 255) %}
{% else %}
Status
{% set min_and_max = 'min=%d max=%d'|format(100, 599) %}
{% endif %}
</label>
<input type="number" name="status_code" id="status_code" {{ min_and_max }} value="{{ status_code }}">
</div>
</div>
<div class="form-group">
<label for="url">
{% if 'command' == profile_type %}
Command
{% else %}
URL
{% endif %}
</label>
<input type="text" name="url" id="url" value="{{ url }}">
</div>
<div class="form-group">
<label for="token">Token</label>
<input type="text" name="token" id="token" size="8" value="{{ token }}">
</div>
<div class="form-group">
<label for="start">From</label>
<input type="date" name="start" id="start" value="{{ start }}">
</div>
<div class="form-group">
<label for="end">Until</label>
<input type="date" name="end" id="end" value="{{ end }}">
</div>
<div class="form-group-row form-group-row-search-button">
<div class="form-group">
<label for="limit">Results</label>
<select name="limit" id="limit">
{% for l in [10, 50, 100] %}
<option {{ l == limit ? 'selected="selected"' }}>{{ l }}</option>
{% endfor %}
</select>
</div>
<input type="hidden" name="type" value="{{ profile_type }}">
<div class="form-group">
<button type="submit" class="btn btn-sm">Search</button>
</div>
</div>
</form>
</div>

View File

@@ -0,0 +1,316 @@
<style>
:root {
--settings-modal-shadow: 0 0 0 1px var(--gray-400), 5px 5px 20px 0 var(--gray-800);
--settings-modal-header-background: var(--gray-200);
--settings-modal-content-background: var(--gray-100);
--settings-option-background: var(--page-background);
--settings-option-border-color: var(--gray-300);
--settings-option-color: var(--color-text);
--settings-option-icon-color: var(--gray-400);
--settings-option-active-border-color: #3b82f6;
--settings-option-active-background: #eff6ff;
--settings-option-active-color: var(--color-text);
--settings-option-active-icon-color: var(--gray-400);
}
.theme-dark {
--settings-modal-shadow: 0 0 0 1px var(--gray-600), 5px 5px 10px 0 var(--gray-900);
--settings-modal-header-background: var(--gray-800);
--settings-modal-content-background: var(--gray-700);
--settings-option-background: transparent;
--settings-option-border-color: var(--gray-500);
--settings-option-color: var(--color-text);
--settings-option-icon-color: var(--gray-300);
--settings-option-active-border-color: #93c5fd;
--settings-option-active-background: var(--gray-700);
--settings-option-active-color: var(--color-text);
--settings-option-active-icon-color: #93c5fd;
}
#open-settings {
color: var(--color-muted);
align-items: center;
display: flex;
margin: 10px 0 5px;
}
#open-settings .icon {
margin-right: 4px;
}
#open-settings .icon, #open-settings svg {
height: 18px;
width: 18px;
}
.modal-wrap {
-webkit-transition-duration: 0.3s;
-webkit-transition-property: opacity, visibility;
-webkit-transition-timing-function: ease-in-out;
align-items: center;
background: rgba(0, 0, 0, 0.70);
display: flex;
backdrop-filter: blur(2px);
height: 100%;
justify-content: center;
left: 0;
opacity: 0;
overflow: auto;
position: fixed;
top: 0;
transition-duration: 0.3s;
transition-property: opacity, visibility;
transition-timing-function: ease-in-out;
visibility: hidden;
width: 100%;
z-index: 100000;
}
.modal-wrap.visible {
opacity: 1;
visibility: visible;
}
.modal-wrap .modal-container {
border-radius: 6px;
box-shadow: var(--settings-modal-shadow);
color: var(--color-text);
margin: 1em;
max-width: 94%;
width: 600px;
}
.modal-wrap .modal-header {
align-items: center;
background: var(--settings-modal-header-background);
border-top-left-radius: 6px;
border-top-right-radius: 6px;
display: flex;
justify-content: space-between;
padding: 15px 30px;
}
.modal-wrap .modal-header h3 {
margin: 0;
}
.modal-wrap .modal-header .close-modal {
background: transparent;
border: 0;
color: var(--color-muted);
cursor: pointer;
font-size: 28px;
line-height: 1;
}
.modal-wrap .modal-header .close-modal:hover { opacity: 1; }
.modal-wrap .modal-content {
background: var(--settings-modal-content-background);
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
margin: 0;
padding: 15px 30px;
z-index: 100000;
}
.modal-content h4 {
font-size: 18px;
margin: 0 0 10px;
}
.modal-content .settings-group + h4 {
margin-top: 30px;
}
.modal-content .settings-group {
border: 1px solid var(--settings-option-border-color);
border-radius: 4px;
display: flex;
margin-bottom: 15px;
}
.modal-content .settings-group label {
cursor: pointer;
display: flex;
flex: 1;
font-size: 16px;
margin: 0;
}
.modal-content .settings-group label input {
position: absolute;
clip: rect(0, 0, 0, 0);
pointer-events: none;
opacity: 0;
}
.modal-content .settings-group:has(input:focus-visible) {
outline: 2px dotted var(--settings-option-active-border-color);
outline-offset: 2px;
}
.modal-content .settings-group label input:checked + p {
box-shadow: inset 0 0 0 2px var(--settings-option-active-border-color);
background-color: var(--settings-option-active-background);
color: var(--settings-option-active-color);
}
.modal-content .settings-group label input:checked + p svg {
color: var(--settings-option-active-icon-color);
}
.modal-content .settings-group label p {
align-items: center;
background: var(--settings-option-background);
color: var(--settings-option-color);
flex: 1;
font-size: 14px;
margin: 0;
padding: 10px 15px;
text-align: center;
}
.modal-content .settings-group label:first-child p {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.modal-content .settings-group label:last-child p {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
.modal-content .settings-group label + label p {
border-left: 1px solid var(--settings-option-border-color);
}
.modal-content label p span {
display: block;
font-size: 16px;
}
.modal-content label p abbr {
border: 0;
text-decoration: none;
}
.modal-content label svg {
color: var(--settings-option-icon-color);
height: 36px;
width: 36px;
}
@media (max-width: 768px) {
#open-settings {
color: transparent;
}
#sidebar:hover #open-settings, #sidebar.expanded #open-settings {
color: var(--color-muted);
}
}
</style>
<a href="#" id="open-settings">
<span class="icon">{{ source('@WebProfiler/Icon/settings.svg') }}</span>
Profiler settings
</a>
<div class="modal-wrap" id="profiler-settings">
<div class="modal-container">
<div class="modal-header">
<h3>Configuration Settings</h3>
<button aria-label="Close" class="close-modal">&times;</button>
</div>
<div class="modal-content">
<h4>Theme</h4>
<div class="settings-group">
<label for="settings-theme-auto">
<input class="config-option" type="radio" name="theme" value="auto" id="settings-theme-auto">
<p>
{{ source('@WebProfiler/Icon/settings-theme-system.svg') }}
<span>System / OS</span>
</p>
</label>
<label for="settings-theme-light">
<input class="config-option" type="radio" name="theme" value="light" id="settings-theme-light">
<p>
{{ source('@WebProfiler/Icon/settings-theme-light.svg') }}
<span>Light</span>
</p>
</label>
<label for="settings-theme-dark">
<input class="config-option" type="radio" name="theme" value="dark" id="settings-theme-dark">
<p>
{{ source('@WebProfiler/Icon/settings-theme-dark.svg') }}
<span>Dark</span>
</p>
</label>
</div>
<h4>Page Width</h4>
<div class="settings-group">
<label for="settings-width-normal">
<input class="config-option" type="radio" name="width" value="normal" id="settings-width-normal">
<p>
{{ source('@WebProfiler/Icon/settings-width-fixed.svg') }}
<span>Fixed width</span>
</p>
</label>
<label for="settings-width-full">
<input class="config-option" type="radio" name="width" value="full" id="settings-width-full">
<p>
{{ source('@WebProfiler/Icon/settings-width-fitted.svg') }}
<span>Fit to window</span>
</p>
</label>
</div>
</div>
</div>
</div>
<script>
(function() {
const configOptions = document.querySelectorAll('.config-option');
[...configOptions].forEach(option => {
option.addEventListener('change', function (event) {
const optionName = option.name;
const optionValue = option.value;
const settingName = 'symfony/profiler/' + optionName;
const settingValue = optionName + '-' + optionValue;
localStorage.setItem(settingName, settingValue);
document.body.classList.forEach((cssClass) => {
if (cssClass.startsWith(optionName)) {
document.body.classList.remove(cssClass);
}
});
const resolvedSettingValue = 'theme-auto' === settingValue
? (matchMedia('(prefers-color-scheme: dark)').matches ? 'theme-dark' : 'theme-light')
: settingValue;
document.body.classList.add(resolvedSettingValue);
if (resolvedSettingValue.startsWith('theme-')) {
document.body.style.colorScheme = resolvedSettingValue.endsWith('-light') ? 'light' : 'dark';
}
});
});
const openModalButton = document.getElementById('open-settings');
const modalWindow = document.getElementById('profiler-settings');
const closeModalButton = document.getElementsByClassName('close-modal')[0];
const modalWrapper = document.getElementsByClassName('modal-wrap')[0];
const closeModal = () => {
modalWindow.classList.remove('visible');
setTimeout(() => openModalButton.focus(), 30);
};
openModalButton.addEventListener('click', function(event) {
document.getElementById('settings-' + (localStorage.getItem('symfony/profiler/theme') || 'theme-auto')).checked = 'checked';
document.getElementById('settings-' + (localStorage.getItem('symfony/profiler/width') || 'width-normal')).checked = 'checked';
modalWindow.classList.toggle('visible');
setTimeout(() => closeModalButton.focus(), 30);
event.preventDefault();
});
closeModalButton.addEventListener('click', closeModal);
modalWrapper.addEventListener('click', function(event) {
if (event.target == event.currentTarget) {
closeModal();
}
});
modalWrapper.addEventListener('keydown', function(event) {
if (event.key === 'Esc' || event.key === 'Escape') {
closeModal();
}
});
})();
</script>

View File

@@ -0,0 +1,18 @@
<div class="table-with-search-field">
<table class="{{ class|default('') }}">
<thead>
<tr>
<th scope="col" class="key">{{ labels is defined ? labels[0] : 'Key' }}</th>
<th scope="col">{{ labels is defined ? labels[1] : 'Value' }}</th>
</tr>
</thead>
<tbody>
{% for key in data|keys|sort %}
<tr>
<th scope="row">{{ key }}</th>
<td>{{ profiler_dump(data[key]) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

View File

@@ -0,0 +1,679 @@
.sf-toolbarreset {
--sf-toolbar-font-family-system: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--sf-toolbar-font-family-monospace: "Ubuntu Mono", "JetBrains Mono", ui-monospace, "Roboto Mono", SFMono-Regular, Menlo, Monaco, Consolas,"Liberation Mono", "Courier New", monospace;
--sf-toolbar-white: #fff;
--sf-toolbar-black: #000;
--sf-toolbar-gray-50: #fafafa;
--sf-toolbar-gray-100: #f5f5f5;
--sf-toolbar-gray-200: #e5e5e5;
--sf-toolbar-gray-300: #d4d4d4;
--sf-toolbar-gray-400: #a3a3a3;
--sf-toolbar-gray-500: #737373;
--sf-toolbar-gray-600: #525252;
--sf-toolbar-gray-700: #404040;
--sf-toolbar-gray-800: #262626;
--sf-toolbar-gray-900: #171717;
--sf-toolbar-red-50: #FEFBFC;
--sf-toolbar-red-100: #FCE9ED;
--sf-toolbar-red-200: #F5B8C5;
--sf-toolbar-red-300: #EF869C;
--sf-toolbar-red-400: #E85574;
--sf-toolbar-red-500: #E1244B;
--sf-toolbar-red-600: #B41939;
--sf-toolbar-red-700: #83122A;
--sf-toolbar-red-800: #510B1A;
--sf-toolbar-red-900: #20040A;
--sf-toolbar-yellow-50: #fef7e1;
--sf-toolbar-yellow-100: #fef2cd;
--sf-toolbar-yellow-200: #fde496;
--sf-toolbar-yellow-300: #fcd55f;
--sf-toolbar-yellow-400: #fbc728;
--sf-toolbar-yellow-500: #e6af05;
--sf-toolbar-yellow-600: #af8503;
--sf-toolbar-yellow-700: #785b02;
--sf-toolbar-yellow-800: #413101;
--sf-toolbar-yellow-900: #0a0800;
--sf-toolbar-green-50: #eff5f5;
--sf-toolbar-green-100: #deeaea;
--sf-toolbar-green-200: #bbd5d5;
--sf-toolbar-green-300: #99bfbf;
--sf-toolbar-green-400: #76a9a9;
--sf-toolbar-green-500: #598e8e;
--sf-toolbar-green-600: #436c6c;
--sf-toolbar-green-700: #2e4949;
--sf-toolbar-green-800: #182727;
--sf-toolbar-green-900: #030404;
}
.sf-toolbar-clearer {
clear: both;
height: 36px;
}
.sf-toolbarreset *:not(svg rect) {
box-sizing: content-box;
vertical-align: baseline;
letter-spacing: normal;
width: auto;
}
.sf-toolbarreset {
background-color: var(--sf-toolbar-gray-800);
bottom: 0;
box-shadow: inset 0 1px 0 var(--sf-toolbar-black), 0 -1px 0 rgba(0, 0, 0, 0.5);
color: var(--sf-toolbar-gray-200);
font: 11px var(--sf-toolbar-font-family-system);
left: 0;
margin: 0;
padding: 0 36px 0 0;
position: fixed;
right: 0;
text-align: left;
text-transform: none;
z-index: 99999;
direction: ltr;
/* neutralize the aliasing defined by external CSS styles */
-webkit-font-smoothing: subpixel-antialiased;
-moz-osx-font-smoothing: auto;
}
.sf-toolbarreset abbr {
border: dashed var(--sf-toolbar-gray-500);
border-width: 0 0 1px;
}
.sf-toolbarreset svg,
.sf-toolbarreset img {
height: 20px;
width: 20px;
display: inline-block;
}
.sf-toolbarreset .sf-cancel-button {
color: var(--sf-toolbar-gray-700);
}
.sf-toolbarreset .sf-toolbar-toggle-button {
background: var(--sf-toolbar-gray-800);
color: var(--sf-toolbar-gray-300);
display: block;
position: absolute;
top: 1px;
right: 0;
width: 36px;
height: 35px;
cursor: pointer;
text-align: center;
border: none;
margin: 0;
padding: 0;
outline: none;
}
.sf-toolbarreset .sf-toolbar-toggle-button:hover {
background: var(--sf-toolbar-gray-700);
}
.sf-toolbar.sf-toolbar-closed .sf-toolbar-clearer {
display: none;
}
.sf-toolbar.sf-toolbar-closed .sf-toolbarreset .sf-toolbar-block {
display: none;
}
.sf-toolbar.sf-toolbar-closed .sf-toolbarreset .sf-toolbar-toggle-button {
top: -37px;
}
.sf-toolbar .sf-toolbar-toggle-button i {
display: block;
height: 35px;
place-content: center;
}
.sf-toolbar.sf-toolbar-opened .sf-toolbar-toggle-button .sf-toolbar-icon-closed {
display: none;
}
.sf-toolbar.sf-toolbar-opened .sf-toolbar-toggle-button .sf-toolbar-icon-opened {
display: block;
}
.sf-toolbar.sf-toolbar-closed .sf-toolbar-toggle-button .sf-toolbar-icon-closed {
display: block;
}
.sf-toolbar.sf-toolbar-closed .sf-toolbar-toggle-button .sf-toolbar-icon-opened {
display: none;
}
.sf-toolbar.sf-toolbar-closed .sf-toolbarreset .sf-toolbar-toggle-button {
border-top: 2px solid var(--sf-toolbar-gray-800);
}
.sf-toolbar-block {
cursor: default;
display: block;
float: left;
height: 36px;
margin-right: 0;
position: relative;
white-space: nowrap;
max-width: 15%;
}
.sf-toolbar-block > a,
.sf-toolbar-block > a:hover {
display: block;
text-decoration: none;
background-color: transparent;
color: inherit;
}
.sf-toolbar-block span {
display: inline-block;
}
.sf-toolbar-block .sf-toolbar-value {
color: var(--sf-toolbar-gray-100);
font-size: 13px;
line-height: 36px;
padding: 0;
}
.sf-toolbar-block .sf-toolbar-label,
.sf-toolbar-block .sf-toolbar-class-separator {
color: var(--sf-toolbar-gray-400);
font-size: 12px;
margin-left: 2px;
}
.sf-toolbar-block .sf-toolbar-info {
border-collapse: collapse;
display: table;
z-index: 100000;
}
.sf-toolbar-block hr {
border-top: 1px solid var(--sf-toolbar-gray-500);
margin: 4px 0;
padding-top: 4px;
}
.sf-toolbar-block .sf-toolbar-info-piece {
/* this 'border-bottom' trick is needed because 'margin-bottom' doesn't work for table rows */
border-bottom: solid transparent 3px;
display: table-row;
}
.sf-toolbar-block .sf-toolbar-info-piece-additional,
.sf-toolbar-block .sf-toolbar-info-piece-additional-detail {
display: none;
}
.sf-toolbar-block .sf-toolbar-info-group {
margin-bottom: 4px;
padding-bottom: 2px;
border-bottom: 1px solid #333333;
}
.sf-toolbar-block .sf-toolbar-info-group:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.sf-toolbar-block .sf-toolbar-info-piece .sf-toolbar-status {
border-radius: 4px;
padding: 2px 5px;
margin-bottom: 0;
}
.sf-toolbar-block .sf-toolbar-info-piece .sf-toolbar-status + .sf-toolbar-status {
margin-left: 4px;
}
.sf-toolbar-block .sf-toolbar-info-piece:last-child {
margin-bottom: 0;
}
div.sf-toolbar .sf-toolbar-block .sf-toolbar-info-piece a {
color: #99CDD8;
text-decoration: underline;
}
div.sf-toolbar .sf-toolbar-block a:hover {
text-decoration: none;
}
.sf-toolbar-block .sf-toolbar-info-piece b {
color: var(--sf-toolbar-gray-400);
display: table-cell;
font-size: 11px;
padding: 4px 8px 4px 0;
}
.sf-toolbar-block:not(.sf-toolbar-block-dump) .sf-toolbar-info-piece span {
color: var(--sf-toolbar-gray-100);
}
.sf-toolbar-block .sf-toolbar-info-piece span {
font-size: 12px;
}
div.sf-toolbar .sf-toolbar-block .sf-toolbar-info-piece.sf-toolbar-info-php-ext a {
text-decoration: none;
}
.sf-toolbar-block .sf-toolbar-info {
background-color: var(--sf-toolbar-gray-700);
border-radius: 4px;
border-bottom-left-radius: 0;
bottom: 36px;
color: var(--sf-toolbar-gray-100);
display: none;
padding: 9px 0;
position: absolute;
}
.sf-toolbar-block.sf-toolbar-block-right .sf-toolbar-info {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 0;
}
.sf-toolbar-block .sf-toolbar-info:empty {
visibility: hidden;
}
.sf-toolbar-block .sf-toolbar-status {
display: inline-block;
color: var(--sf-toolbar-white);
background-color: var(--sf-toolbar-gray-600);
padding: 3px 6px;
margin: 0 4px;
min-width: 15px;
min-height: 13px;
text-align: center;
}
.sf-toolbar-block .sf-toolbar-status.sf-toolbar-status-green,
.sf-toolbar-block .sf-toolbar-info .sf-toolbar-status.sf-toolbar-status-green {
background-color: #059669;
color: var(--white);
}
.sf-toolbar-block .sf-toolbar-status.sf-toolbar-status-red,
.sf-toolbar-block .sf-toolbar-info .sf-toolbar-status.sf-toolbar-status-red {
background-color: var(--sf-toolbar-red-500);
color: var(--sf-toolbar-red-50);
}
.sf-toolbar-block .sf-toolbar-status.sf-toolbar-status-yellow,
.sf-toolbar-block .sf-toolbar-info .sf-toolbar-status.sf-toolbar-status-yellow {
background-color: var(--sf-toolbar-yellow-300);
color: var(--sf-toolbar-yellow-800);
}
.sf-toolbar-block.sf-toolbar-status-green::before,
.sf-toolbar-block.sf-toolbar-status-red::before,
.sf-toolbar-block.sf-toolbar-status-yellow::before {
background: var(--sf-toolbar-yellow-400);
border-radius: 6px;
content: '';
position: absolute;
bottom: 1px;
left: 0;
width: 98%;
height: 3px;
z-index: 10005;
}
.sf-toolbar-block.sf-toolbar-status-red::before {
background: var(--sf-toolbar-red-400);
}
.sf-toolbar-block.sf-toolbar-status-green::before {
background: var(--sf-toolbar-green-400);
}
.sf-toolbar-block-request.sf-toolbar-block.sf-toolbar-status-green::before,
.sf-toolbar-block-request.sf-toolbar-block.sf-toolbar-status-red::before,
.sf-toolbar-block-request.sf-toolbar-block.sf-toolbar-status-yellow::before {
display: none;
}
.sf-toolbar-block-request .sf-toolbar-status {
border-radius: 6px;
color: #fff;
display: inline-block;
flex-shrink: 0;
font-size: 13px;
font-weight: 500;
padding: 4px 8px;
}
.sf-toolbar-block-request .sf-toolbar-info-piece a {
background-color: transparent;
text-decoration: none;
}
.sf-toolbar-block-request .sf-toolbar-info-piece a:hover {
text-decoration: underline;
}
.sf-toolbar-block-request .sf-toolbar-redirection-status {
font-weight: normal;
padding: 2px 4px;
line-height: 18px;
}
.sf-toolbar-block.sf-toolbar-block-request .sf-toolbar-redirection-status.sf-toolbar-status-yellow {
background-color: var(--sf-toolbar-yellow-300);
border-radius: 4px;
color: var(--sf-toolbar-yellow-800);
padding: 1px 4px;
}
.sf-toolbar-block.sf-toolbar-block-request .sf-toolbar-info-piece .sf-toolbar-redirection-method {
background: transparent;
color: var(--sf-toolbar-gray-300);
border: 1px solid var(--sf-toolbar-gray-400);
padding: 1px 4px;
}
.sf-toolbar-block-request .sf-toolbar-info-piece span.sf-toolbar-redirection-method {
font-size: 12px;
height: 17px;
line-height: 17px;
margin-right: 5px;
}
.sf-toolbar-block-request .sf-toolbar-request-icon svg {
stroke-width: 3px;
}
.sf-toolbar-block-ajax .sf-toolbar-icon {
cursor: pointer;
}
.sf-toolbar-status-green .sf-toolbar-label,
.sf-toolbar-status-yellow .sf-toolbar-label,
.sf-toolbar-status-red .sf-toolbar-label {
color: var(--sf-toolbar-white);
}
.sf-toolbar-block-config svg path,
.sf-toolbar-block-config svg .sf-svg-path {
fill: var(--sf-toolbar-white);
}
.sf-toolbar-block .sf-toolbar-icon {
color: var(--sf-toolbar-gray-300);
align-items: center;
display: flex;
height: 36px;
padding: 0 7px;
overflow: hidden;
text-overflow: ellipsis;
}
.sf-toolbar-block:hover .sf-toolbar-icon {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
box-shadow: 1px 0 0 var(--sf-toolbar-black), inset 0 -1px 0 var(--sf-toolbar-black);
}
.sf-toolbar-block.sf-toolbar-block-right:hover .sf-toolbar-icon {
box-shadow: -1px 0 0 var(--sf-toolbar-black), inset 0 -1px 0 var(--sf-toolbar-black);
}
.sf-toolbar-block-request .sf-toolbar-icon {
padding-left: 0;
padding-right: 0;
}
.sf-toolbar-block .sf-toolbar-icon img,
.sf-toolbar-block .sf-toolbar-icon svg {
border-width: 0;
}
.sf-toolbar-block .sf-toolbar-icon img + span,
.sf-toolbar-block .sf-toolbar-icon svg + span {
margin-left: 4px;
}
.sf-toolbar-block-config .sf-toolbar-icon .sf-toolbar-value,
.sf-toolbar-block.sf-toolbar-block-sf-cli .sf-toolbar-value {
margin-left: 5px;
}
.sf-toolbar-block-config .sf-toolbar-icon .sf-toolbar-label,
.sf-toolbar-block.sf-toolbar-block-sf-cli .sf-toolbar-label {
margin-left: 0;
}
.sf-toolbar-block:hover,
.sf-toolbar-block.hover {
position: relative;
}
.sf-toolbar-block:hover .sf-toolbar-icon,
.sf-toolbar-block.hover .sf-toolbar-icon {
background-color: var(--sf-toolbar-gray-700);
position: relative;
z-index: 10002;
}
.sf-toolbar-block-ajax.hover .sf-toolbar-info {
z-index: 10001;
}
.sf-toolbar-block:hover .sf-toolbar-info,
.sf-toolbar-block.hover .sf-toolbar-info {
display: block;
padding: 10px;
max-width: 525px;
max-height: 480px;
word-wrap: break-word;
overflow: hidden;
overflow-y: auto;
}
.sf-toolbar-info-piece b.sf-toolbar-ajax-info {
color: var(--sf-toolbar-gray-100);
}
.sf-toolbar-ajax-requests {
border: 1px solid var(--sf-toolbar-gray-500);
font-variant: tabular-nums;
margin: 5px 0 0;
width: 100%;
}
.sf-toolbar-ajax-requests td {
background-color: var(--sf-toolbar-gray-700);
border: 1px solid var(--sf-toolbar-gray-500);
color: var(--sf-toolbar-gray-100);
font-size: 12px;
padding: 4px;
vertical-align: middle;
}
.sf-toolbar-ajax-requests thead {
border: 0;
}
.sf-toolbar-ajax-requests th {
background-color: var(--sf-toolbar-gray-800);
border: 1px solid var(--sf-toolbar-gray-500);
color: var(--sf-toolbar-gray-200);
font-size: 11px;
padding: 4px;
}
.sf-ajax-request-url {
max-width: 250px;
line-height: 9px;
overflow: hidden;
text-overflow: ellipsis;
}
.sf-toolbar-ajax-requests .sf-ajax-request-url a {
text-decoration: none;
}
.sf-toolbar-ajax-requests .sf-ajax-request-url a:hover {
text-decoration: underline;
}
.sf-ajax-request-duration {
text-align: right;
}
.sf-toolbar-block .sf-toolbar-info-piece .sf-toolbar-ajax-requests .sf-toolbar-status {
font-size: 11px;
padding: 1px 3px;
}
.sf-ajax-request-loading {
animation: sf-blink .5s ease-in-out infinite;
}
@keyframes sf-blink {
0% { background: var(--sf-toolbar-gray-800); }
50% { background: var(--sf-toolbar-gray-700); }
100% { background: var(--sf-toolbar-gray-800); }
}
.sf-toolbar-block.sf-toolbar-block-dump .sf-toolbar-info {
max-width: none;
width: 100%;
position: fixed;
box-sizing: border-box;
left: 0;
}
.sf-toolbar-block-dump pre.sf-dump {
background-color: var(--sf-toolbar-gray-800);
border-color: var(--sf-toolbar-gray-500);
border-radius: 0;
margin: 6px 0 12px 0;
}
.sf-toolbar-block-dump pre.sf-dump:last-child {
margin-bottom: 0;
}
.sf-toolbar-block-dump pre.sf-dump .sf-dump-search-wrapper {
margin-bottom: 5px;
}
.sf-toolbar-block-dump pre.sf-dump span.sf-dump-search-count {
color: #333;
font-size: 12px;
}
.sf-toolbar-block-dump .sf-toolbar-info-piece {
display: block;
}
.sf-toolbar-block-dump .sf-toolbar-info-piece .sf-toolbar-file-line {
color: var(--sf-toolbar-gray-400);
margin-left: 4px;
}
.sf-toolbar-block-dump .sf-toolbar-info img {
display: none;
}
.sf-toolbar-block-serializer .detailed-metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 15px;
margin-top: 15px;
}
/* Responsive Design */
.sf-toolbar-icon .sf-toolbar-label,
.sf-toolbar-icon .sf-toolbar-value {
display: none;
}
.sf-toolbar-block-config .sf-toolbar-icon .sf-toolbar-label,
.sf-cli .sf-toolbar-icon .sf-toolbar-label {
display: inline-block;
}
/* Legacy Design - these styles are maintained to make old panels look
a bit better on the new toolbar */
.sf-toolbar-block .sf-toolbar-info-piece-additional-detail {
color: var(--sf-toolbar-gray-400);
font-size: 12px;
}
.sf-toolbar-status-green .sf-toolbar-info-piece-additional-detail,
.sf-toolbar-status-yellow .sf-toolbar-info-piece-additional-detail,
.sf-toolbar-status-red .sf-toolbar-info-piece-additional-detail {
color: var(--sf-toolbar-white);
}
@media (min-width: 768px) {
.sf-toolbar-icon .sf-toolbar-label,
.sf-toolbar-icon .sf-toolbar-value {
display: inline;
}
.sf-toolbar-block-time .sf-toolbar-icon svg,
.sf-toolbar-block-memory .sf-toolbar-icon svg {
display: none;
}
.sf-toolbar-block-time .sf-toolbar-icon svg + span,
.sf-toolbar-block-memory .sf-toolbar-icon svg + span {
margin-left: 0;
}
.sf-toolbar-block .sf-toolbar-icon {
padding: 0 10px;
}
.sf-toolbar-block-time .sf-toolbar-icon {
padding-right: 5px;
}
.sf-toolbar-block-memory .sf-toolbar-icon {
padding-left: 5px;
}
.sf-toolbar-block-request .sf-toolbar-icon {
display: flex;
align-items: center;
padding-left: 0;
padding-right: 0;
}
.sf-toolbar-block-request .sf-toolbar-label {
margin-left: 4px;
margin-right: 1px;
}
.sf-toolbar-block-request .sf-toolbar-status + .sf-toolbar-request-icon {
display: inline-flex;
margin-left: 5px;
}
.sf-toolbar-block-request .sf-toolbar-icon .sf-toolbar-request-icon + .sf-toolbar-label {
margin-left: 0;
}
.sf-toolbar-block-request .sf-toolbar-label + .sf-toolbar-value {
margin-right: 5px;
}
.sf-toolbar-block-request:hover .sf-toolbar-info {
max-width: none;
}
.sf-toolbar-block .sf-toolbar-info-piece b {
font-size: 12px;
}
.sf-toolbar-block .sf-toolbar-info-piece span {
font-size: 13px;
}
.sf-toolbar-block-right {
float: right;
margin-left: 0;
margin-right: 0;
}
.sf-toolbarreset .sf-toolbar-block.sf-toolbar-block-right:not(.sf-toolbar-block-sf-cli) .sf-toolbar-info {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 0;
}
}
@media (min-width: 1024px) {
.sf-toolbar-block .sf-toolbar-info-piece-additional,
.sf-toolbar-block .sf-toolbar-info-piece-additional-detail {
display: inline;
}
.sf-toolbar-block .sf-toolbar-info-piece-additional:empty,
.sf-toolbar-block .sf-toolbar-info-piece-additional-detail:empty {
display: none;
}
}
/***** Error Toolbar *****/
.sf-error-toolbar .sf-toolbarreset {
background: var(--sf-toolbar-gray-800);
color: var(--sf-toolbar-gray-100);
font: 13px/36px var(--sf-toolbar-font-family-system);
height: 36px;
padding: 0 15px;
text-align: left;
}
.sf-error-toolbar .sf-toolbarreset svg {
height: auto;
}
.sf-error-toolbar .sf-toolbarreset a {
color: #99cdd8;
margin-left: 5px;
text-decoration: underline;
}
.sf-error-toolbar .sf-toolbarreset a:hover {
text-decoration: none;
}
.sf-error-toolbar .sf-toolbarreset .sf-toolbar-icon {
float: left;
padding: 5px 0;
margin-right: 10px;
}
.sf-full-stack {
left: 0px;
font-size: 12px;
}
/***** Media query print: Do not print the Toolbar. *****/
@media print {
.sf-toolbar {
display: none !important;
}
}

View File

@@ -0,0 +1,39 @@
<div id="sfToolbarClearer-{{ token }}" class="sf-toolbar-clearer"></div>
<div id="sfToolbarMainContent-{{ token }}" class="sf-toolbarreset notranslate clear-fix" data-no-turbolink data-turbo="false">
{% for name, template in templates %}
{% if block('toolbar', template) is defined %}
{% with {
collector: profile ? profile.getcollector(name) : null,
profiler_url: profiler_url,
token: token ?? (profile ? profile.token : null),
name: name,
profiler_markup_version: profiler_markup_version,
csp_script_nonce: csp_script_nonce,
csp_style_nonce: csp_style_nonce
} %}
{{ block('toolbar', template) }}
{% endwith %}
{% endif %}
{% endfor %}
{% if full_stack %}
<div class="sf-full-stack sf-toolbar-block sf-toolbar-block-full-stack sf-toolbar-status-red sf-toolbar-block-right">
<div class="sf-toolbar-icon">
<span class="sf-toolbar-value">Using symfony/symfony is NOT supported</span>
</div>
<div class="sf-toolbar-info sf-toolbar-status-red">
<p>This project is using Symfony via the "symfony/symfony" package.</p>
<p>This is NOT supported anymore since Symfony 4.0.</p>
<p>Even if it seems to work well, it has some important limitations with no workarounds.</p>
<p>Using this package also makes your project slower.</p>
<strong>Please, stop using this package and replace it with individual packages instead.</strong>
</div>
<div></div>
</div>
{% endif %}
<button class="sf-toolbar-toggle-button" type="button" id="sfToolbarToggleButton-{{ token }}" accesskey="D" aria-expanded="true" aria-controls="sfToolbarMainContent-{{ token }}">
<i class="sf-toolbar-icon-opened" title="Close Toolbar">{{ source('@WebProfiler/Icon/close.svg') }}</i>
<i class="sf-toolbar-icon-closed" title="Open Toolbar">{{ source('@WebProfiler/Icon/symfony.svg') }}</i>
</button>
</div>

View File

@@ -0,0 +1,6 @@
<div class="sf-toolbar-block sf-toolbar-block-{{ name }} sf-toolbar-status-{{ status|default('normal') }} {{ additional_classes|default('') }}" {{ block_attrs|default('')|raw }}>
{% if link is not defined or link %}<a href="{{ url('_profiler', { token: token, panel: name }) }}">{% endif %}
<div class="sf-toolbar-icon">{{ icon|default('') }}</div>
{% if link|default(false) %}</a>{% endif %}
<div class="sf-toolbar-info">{{ text|default('') }}</div>
</div>

View File

@@ -0,0 +1,632 @@
<!-- START of Symfony Web Debug Toolbar -->
<div id="sfwdt{{ token }}" class="sf-toolbar sf-toolbar-opened" role="region" aria-label="Symfony Web Debug Toolbar">
{{ include('@WebProfiler/Profiler/toolbar.html.twig', {
templates: {
'request': '@WebProfiler/Profiler/cancel.html.twig'
},
profile: null,
profiler_url: url('_profiler', {token: token}),
profiler_markup_version: 3,
}) }}
</div>
<link rel="stylesheet"{% if csp_style_nonce %} nonce="{{ csp_style_nonce }}"{% endif %} href="{{ url('_wdt_stylesheet') }}" />
{# CAUTION: the contents of this file are processed by Twig before loading
them as JavaScript source code. Always use '/*' comments instead
of '//' comments to avoid impossible-to-debug side-effects #}
<script{% if csp_script_nonce is defined and csp_script_nonce %} nonce="{{ csp_script_nonce }}"{% endif %}>/*<![CDATA[*/
if (typeof Sfjs === 'undefined' || typeof Sfjs.loadToolbar === 'undefined') {
Sfjs = (function() {
"use strict";
if ('classList' in document.documentElement) {
var hasClass = function (el, cssClass) { return el.classList.contains(cssClass); };
var removeClass = function(el, cssClass) { el.classList.remove(cssClass); };
var addClass = function(el, cssClass) { el.classList.add(cssClass); };
var toggleClass = function(el, cssClass) { el.classList.toggle(cssClass); };
} else {
var hasClass = function (el, cssClass) { return el.className.match(new RegExp('\\b' + cssClass + '\\b')); };
var removeClass = function(el, cssClass) { el.className = el.className.replace(new RegExp('\\b' + cssClass + '\\b'), ' '); };
var addClass = function(el, cssClass) { if (!hasClass(el, cssClass)) { el.className += " " + cssClass; } };
var toggleClass = function(el, cssClass) { hasClass(el, cssClass) ? removeClass(el, cssClass) : addClass(el, cssClass); };
}
var noop = function() {};
var profilerStorageKey = 'symfony/profiler/';
var addEventListener;
var el = document.createElement('div');
if (!('addEventListener' in el)) {
addEventListener = function (element, eventName, callback) {
element.attachEvent('on' + eventName, callback);
};
} else {
addEventListener = function (element, eventName, callback) {
element.addEventListener(eventName, callback, false);
};
}
var request = function(url, onSuccess, onError, payload, options, tries) {
url = new URL(url);
url.searchParams.set('XDEBUG_IGNORE', '1');
url = url.toString();
var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
options = options || {};
options.retry = options.retry || false;
tries = tries || 1;
/* this delays for 125, 375, 625, 875, and 1000, ... */
var delay = tries < 5 ? (tries - 0.5) * 250 : 1000;
xhr.open(options.method || 'GET', url, true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.onreadystatechange = function(state) {
if (4 !== xhr.readyState) {
return null;
}
if (xhr.status == 404 && options.retry && !options.stop) {
setTimeout(function() {
if (options.stop) {
return;
}
request(url, onSuccess, onError, payload, options, tries + 1);
}, delay);
return null;
}
if (200 === xhr.status) {
(onSuccess || noop)(xhr);
} else {
(onError || noop)(xhr);
}
};
if (options.onSend) {
options.onSend(tries);
}
xhr.send(payload || '');
};
var getPreference = function(name) {
if (!window.localStorage) {
return null;
}
return localStorage.getItem(profilerStorageKey + name);
};
var setPreference = function(name, value) {
if (!window.localStorage) {
return null;
}
localStorage.setItem(profilerStorageKey + name, value);
};
var requestStack = [];
var extractHeaders = function(xhr, stackElement) {
/* Here we avoid to call xhr.getResponseHeader in order to */
/* prevent polluting the console with CORS security errors */
var allHeaders = xhr.getAllResponseHeaders();
var ret;
if (ret = allHeaders.match(/^x-debug-token:\s+(.*)$/im)) {
stackElement.profile = ret[1];
}
if (ret = allHeaders.match(/^x-debug-token-link:\s+(.*)$/im)) {
stackElement.profilerUrl = ret[1];
}
if (ret = allHeaders.match(/^Symfony-Debug-Toolbar-Replace:\s+(.*)$/im)) {
stackElement.toolbarReplaceFinished = false;
stackElement.toolbarReplace = '1' === ret[1];
}
};
var successStreak = 4;
var pendingRequests = 0;
var renderAjaxRequests = function() {
var requestCounter = document.querySelector('.sf-toolbar-ajax-request-counter');
if (!requestCounter) {
return;
}
requestCounter.textContent = requestStack.length;
var infoSpan = document.querySelector(".sf-toolbar-ajax-info");
if (infoSpan) {
infoSpan.textContent = requestStack.length + ' AJAX request' + (requestStack.length !== 1 ? 's' : '');
}
var ajaxToolbarPanel = document.querySelector('.sf-toolbar-block-ajax');
if (requestStack.length) {
ajaxToolbarPanel.style.display = '';
} else {
ajaxToolbarPanel.style.display = 'none';
}
if (pendingRequests > 0) {
addClass(ajaxToolbarPanel, 'sf-ajax-request-loading');
} else if (successStreak < 4) {
addClass(ajaxToolbarPanel, 'sf-toolbar-status-red');
removeClass(ajaxToolbarPanel, 'sf-ajax-request-loading');
} else {
removeClass(ajaxToolbarPanel, 'sf-ajax-request-loading');
removeClass(ajaxToolbarPanel, 'sf-toolbar-status-red');
}
};
var startAjaxRequest = function(index) {
var tbody = document.querySelector('.sf-toolbar-ajax-request-list');
if (!tbody) {
return;
}
var nbOfAjaxRequest = tbody.rows.length;
if (nbOfAjaxRequest >= 100) {
tbody.deleteRow(0);
}
var request = requestStack[index];
pendingRequests++;
var row = document.createElement('tr');
request.DOMNode = row;
var requestNumberCell = document.createElement('td');
requestNumberCell.textContent = index + 1;
row.appendChild(requestNumberCell);
var profilerCell = document.createElement('td');
profilerCell.textContent = 'n/a';
row.appendChild(profilerCell);
var methodCell = document.createElement('td');
methodCell.textContent = request.method;
row.appendChild(methodCell);
var typeCell = document.createElement('td');
typeCell.textContent = request.type;
row.appendChild(typeCell);
var statusCodeCell = document.createElement('td');
var statusCode = document.createElement('span');
statusCode.textContent = 'n/a';
statusCodeCell.appendChild(statusCode);
row.appendChild(statusCodeCell);
var pathCell = document.createElement('td');
pathCell.className = 'sf-ajax-request-url';
if ('GET' === request.method) {
var pathLink = document.createElement('a');
pathLink.setAttribute('href', request.url);
pathLink.textContent = request.url;
pathCell.appendChild(pathLink);
} else {
pathCell.textContent = request.url;
}
pathCell.setAttribute('title', request.url);
row.appendChild(pathCell);
var durationCell = document.createElement('td');
durationCell.className = 'sf-ajax-request-duration';
durationCell.textContent = 'n/a';
row.appendChild(durationCell);
request.liveDurationHandle = setInterval(function() {
durationCell.textContent = (new Date() - request.start) + ' ms';
}, 100);
row.className = 'sf-ajax-request sf-ajax-request-loading';
tbody.insertBefore(row, null);
var toolbarInfo = document.querySelector('.sf-toolbar-block-ajax .sf-toolbar-info');
toolbarInfo.scrollTop = toolbarInfo.scrollHeight;
renderAjaxRequests();
};
var finishAjaxRequest = function(index) {
var request = requestStack[index];
clearInterval(request.liveDurationHandle);
if (!request.DOMNode) {
return;
}
if (request.toolbarReplace && !request.toolbarReplaceFinished && request.profile) {
/* Flag as complete because finishAjaxRequest can be called multiple times. */
request.toolbarReplaceFinished = true;
/* Search up through the DOM to find the toolbar's container ID. */
for (var elem = request.DOMNode; elem && elem !== document; elem = elem.parentNode) {
if (elem.id.match(/^sfwdt/)) {
Sfjs.loadToolbar(elem.id.replace(/^sfwdt/, ''), request.profile);
break;
}
}
}
pendingRequests--;
var row = request.DOMNode;
/* Unpack the children from the row */
var profilerCell = row.children[1];
var methodCell = row.children[2];
var statusCodeCell = row.children[4];
var statusCodeElem = statusCodeCell.children[0];
var durationCell = row.children[6];
if (request.error) {
row.className = 'sf-ajax-request sf-ajax-request-error';
methodCell.className = 'sf-ajax-request-error';
successStreak = 0;
} else {
row.className = 'sf-ajax-request sf-ajax-request-ok';
successStreak++;
}
if (request.statusCode) {
if (request.statusCode < 300) {
statusCodeElem.setAttribute('class', 'sf-toolbar-status');
} else if (request.statusCode < 400) {
statusCodeElem.setAttribute('class', 'sf-toolbar-status sf-toolbar-status-yellow');
} else {
statusCodeElem.setAttribute('class', 'sf-toolbar-status sf-toolbar-status-red');
}
statusCodeElem.textContent = request.statusCode;
} else {
statusCodeElem.setAttribute('class', 'sf-toolbar-status sf-toolbar-status-red');
}
if (request.duration) {
durationCell.textContent = request.duration + ' ms';
}
if (request.profilerUrl) {
profilerCell.textContent = '';
var profilerLink = document.createElement('a');
profilerLink.setAttribute('href', request.profilerUrl);
profilerLink.textContent = request.profile;
profilerCell.appendChild(profilerLink);
}
renderAjaxRequests();
};
{% if excluded_ajax_paths is defined %}
if (window.fetch && window.fetch.polyfill === undefined) {
var oldFetch = window.fetch;
window.fetch = function () {
var promise = oldFetch.apply(this, arguments);
var url = arguments[0];
var params = arguments[1];
var paramType = Object.prototype.toString.call(arguments[0]);
if (paramType === '[object Request]') {
url = arguments[0].url;
params = {
method: arguments[0].method,
credentials: arguments[0].credentials,
headers: arguments[0].headers,
mode: arguments[0].mode,
redirect: arguments[0].redirect
};
} else {
url = String(url);
}
if (!url.match(new RegExp({{ excluded_ajax_paths|json_encode|raw }}))) {
var method = 'GET';
if (params && params.method !== undefined) {
method = params.method;
}
var stackElement = {
error: false,
url: url,
method: method,
type: 'fetch',
start: new Date()
};
var idx = requestStack.push(stackElement) - 1;
promise.then(function (r) {
stackElement.duration = new Date() - stackElement.start;
stackElement.error = r.status < 200 || r.status >= 400;
stackElement.statusCode = r.status;
stackElement.profile = r.headers.get('x-debug-token');
stackElement.profilerUrl = r.headers.get('x-debug-token-link');
stackElement.toolbarReplaceFinished = false;
stackElement.toolbarReplace = '1' === r.headers.get('Symfony-Debug-Toolbar-Replace');
finishAjaxRequest(idx);
}, function (e){
stackElement.error = true;
finishAjaxRequest(idx);
});
startAjaxRequest(idx);
}
return promise;
};
}
if (window.XMLHttpRequest && XMLHttpRequest.prototype.addEventListener) {
var proxied = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, async, user, pass) {
var self = this;
/* prevent logging AJAX calls to static and inline files, like templates */
var path = url;
if (url.slice(0, 1) === '/') {
if (0 === url.indexOf('{{ request.basePath|e('js') }}')) {
path = url.slice({{ request.basePath|length }});
}
}
else if (0 === url.indexOf('{{ (request.schemeAndHttpHost ~ request.basePath)|e('js') }}')) {
path = url.slice({{ (request.schemeAndHttpHost ~ request.basePath)|length }});
}
if (!path.match(new RegExp({{ excluded_ajax_paths|json_encode|raw }}))) {
var stackElement = {
error: false,
url: url,
method: method,
type: 'xhr',
start: new Date()
};
var idx = requestStack.push(stackElement) - 1;
this.addEventListener('readystatechange', function() {
if (self.readyState == 4) {
stackElement.duration = new Date() - stackElement.start;
stackElement.error = self.status < 200 || self.status >= 400;
stackElement.statusCode = self.status;
extractHeaders(self, stackElement);
finishAjaxRequest(idx);
}
}, false);
startAjaxRequest(idx);
}
proxied.apply(this, Array.prototype.slice.call(arguments));
};
}
{% endif %}
return {
hasClass: hasClass,
removeClass: removeClass,
addClass: addClass,
toggleClass: toggleClass,
getPreference: getPreference,
setPreference: setPreference,
addEventListener: addEventListener,
request: request,
renderAjaxRequests: renderAjaxRequests,
getSfwdt: function(token) {
return document.getElementById('sfwdt' + token);
},
load: function(selector, url, onSuccess, onError, options) {
var el = document.getElementById(selector);
if (el && el.getAttribute('data-sfurl') !== url) {
request(
url,
function(xhr) {
el.innerHTML = xhr.responseText;
el.setAttribute('data-sfurl', url);
removeClass(el, 'loading');
var pending = pendingRequests;
for (var i = 0; i < requestStack.length; i++) {
startAjaxRequest(i);
if (requestStack[i].duration) {
finishAjaxRequest(i);
}
}
/* Revert the pending state in case there was a start called without a finish above. */
pendingRequests = pending;
(onSuccess || noop)(xhr, el);
},
function(xhr) { (onError || noop)(xhr, el); },
'',
options
);
}
return this;
},
showToolbar: function(token) {
var sfwdt = this.getSfwdt(token);
if ('closed' === getPreference('toolbar/displayState')) {
addClass(sfwdt, 'sf-toolbar-closed');
removeClass(sfwdt, 'sf-toolbar-opened');
} else {
addClass(sfwdt, 'sf-toolbar-opened');
removeClass(sfwdt, 'sf-toolbar-closed');
}
},
hideToolbar: function(token) {
var sfwdt = this.getSfwdt(token);
addClass(sfwdt, 'sf-toolbar-closed');
removeClass(sfwdt, 'sf-toolbar-opened');
},
initToolbar: function(token) {
this.showToolbar(token);
var toggleButton = document.querySelector(`#sfToolbarToggleButton-${token}`);
addEventListener(toggleButton, 'click', function (event) {
event.preventDefault();
const newState = 'opened' === getPreference('toolbar/displayState') ? 'closed' : 'opened';
setPreference('toolbar/displayState', newState);
'opened' === newState ? Sfjs.showToolbar(token) : Sfjs.hideToolbar(token);
});
},
loadToolbar: function(token, newToken) {
var that = this;
var triesCounter = document.getElementById('sfLoadCounter-' + token);
var options = {
retry: true,
onSend: function (count) {
if (count === 3) {
that.initToolbar(token);
}
if (triesCounter) {
triesCounter.textContent = count;
}
},
};
var cancelButton = document.getElementById('sfLoadCancel-' + token);
if (cancelButton) {
addEventListener(cancelButton, 'click', function (event) {
event.preventDefault();
options.stop = true;
that.hideToolbar(token);
});
}
newToken = (newToken || token);
this.load(
'sfwdt' + token,
'{{ url("_wdt", { "token": "xxxxxx" })|escape('js') }}'.replace(/xxxxxx/, newToken),
function(xhr, el) {
var toolbarContent = document.getElementById('sfToolbarMainContent-' + newToken);
/* Do nothing in the edge case where the toolbar has already been replaced with a new one */
if (!toolbarContent) {
return;
}
/* Replace the ID, it has to match the new token */
toolbarContent.parentElement.id = 'sfwdt' + newToken;
/* Evaluate in global scope scripts embedded inside the toolbar */
var i, scripts = [].slice.call(el.querySelectorAll('script'));
for (i = 0; i < scripts.length; ++i) {
if (scripts[i].firstChild) {
eval.call({}, scripts[i].firstChild.nodeValue);
}
}
el.style.display = -1 !== xhr.responseText.indexOf('sf-toolbarreset') ? 'block' : 'none';
if (el.style.display == 'none') {
return;
}
that.initToolbar(newToken);
/* Handle toolbar-info position */
var toolbarBlocks = [].slice.call(el.querySelectorAll('.sf-toolbar-block'));
for (i = 0; i < toolbarBlocks.length; ++i) {
toolbarBlocks[i].onmouseover = function () {
var toolbarInfo = this.querySelectorAll('.sf-toolbar-info')[0];
var pageWidth = document.body.clientWidth;
var elementWidth = toolbarInfo.offsetWidth;
var leftValue = (elementWidth + this.offsetLeft) - pageWidth;
var rightValue = (elementWidth + (pageWidth - this.offsetLeft)) - pageWidth;
/* Reset right and left value, useful on window resize */
toolbarInfo.style.right = '';
toolbarInfo.style.left = '';
if (elementWidth > pageWidth) {
toolbarInfo.style.left = 0;
}
else if (leftValue > 0 && rightValue > 0) {
toolbarInfo.style.right = (rightValue * -1) + 'px';
} else if (leftValue < 0) {
toolbarInfo.style.left = 0;
} else {
toolbarInfo.style.right = '0px';
}
};
}
renderAjaxRequests();
addEventListener(document.querySelector('.sf-toolbar-ajax-clear'), 'click', function() {
requestStack = [];
renderAjaxRequests();
successStreak = 4;
document.querySelector('.sf-toolbar-ajax-request-list').innerHTML = '';
});
addEventListener(document.querySelector('.sf-toolbar-block-ajax'), 'mouseenter', function (event) {
var elem = document.querySelector('.sf-toolbar-block-ajax .sf-toolbar-info');
elem.scrollTop = elem.scrollHeight;
});
addEventListener(document.querySelector('.sf-toolbar-block-ajax > .sf-toolbar-icon'), 'click', function (event) {
event.preventDefault();
toggleClass(this.parentNode, 'hover');
});
var dumpInfo = document.querySelector('.sf-toolbar-block-dump .sf-toolbar-info');
if (null !== dumpInfo) {
addEventListener(dumpInfo, 'sfbeforedumpcollapse', function () {
dumpInfo.style.minHeight = dumpInfo.getBoundingClientRect().height+'px';
});
addEventListener(dumpInfo, 'mouseleave', function () {
dumpInfo.style.minHeight = '';
});
}
},
function(xhr) {
if (xhr.status !== 0 && !options.stop) {
var sfwdt = that.getSfwdt(token);
sfwdt.innerHTML = '\
<div class="sf-toolbarreset notranslate">\
<div class="sf-toolbar-icon"><svg width="26" height="28" xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" viewBox="0 0 26 28" enable-background="new 0 0 26 28" xml:space="preserve"><path fill="#FFFFFF" d="M13 0C5.8 0 0 5.8 0 13c0 7.2 5.8 13 13 13c7.2 0 13-5.8 13-13C26 5.8 20.2 0 13 0z M20 7.5 c-0.6 0-1-0.3-1-0.9c0-0.2 0-0.4 0.2-0.6c0.1-0.3 0.2-0.3 0.2-0.4c0-0.3-0.5-0.4-0.7-0.4c-2 0.1-2.5 2.7-2.9 4.8l-0.2 1.1 c1.1 0.2 1.9 0 2.4-0.3c0.6-0.4-0.2-0.8-0.1-1.3C18 9.2 18.4 9 18.7 8.9c0.5 0 0.8 0.5 0.8 1c0 0.8-1.1 2-3.3 1.9 c-0.3 0-0.5 0-0.7-0.1L15 14.1c-0.4 1.7-0.9 4.1-2.6 6.2c-1.5 1.8-3.1 2.1-3.8 2.1c-1.3 0-2.1-0.6-2.2-1.6c0-0.9 0.8-1.4 1.3-1.4 c0.7 0 1.2 0.5 1.2 1.1c0 0.5-0.2 0.6-0.4 0.7c-0.1 0.1-0.3 0.2-0.3 0.4c0 0.1 0.1 0.3 0.4 0.3c0.5 0 0.9-0.3 1.2-0.5 c1.3-1 1.7-2.9 2.4-6.2l0.1-0.8c0.2-1.1 0.5-2.3 0.8-3.5c-0.9-0.7-1.4-1.5-2.6-1.8c-0.8-0.2-1.3 0-1.7 0.4C8.4 10 8.6 10.7 9 11.1 l0.7 0.7c0.8 0.9 1.3 1.7 1.1 2.7c-0.3 1.6-2.1 2.8-4.3 2.1c-1.9-0.6-2.2-1.9-2-2.7c0.2-0.6 0.7-0.8 1.2-0.6 c0.5 0.2 0.7 0.8 0.6 1.3c0 0.1 0 0.1-0.1 0.3C6 15 5.9 15.2 5.9 15.3c-0.1 0.4 0.4 0.7 0.8 0.8c0.8 0.3 1.7-0.2 1.9-0.9 c0.2-0.6-0.2-1.1-0.4-1.2l-0.8-0.9c-0.4-0.4-1.2-1.5-0.8-2.8c0.2-0.5 0.5-1 0.9-1.4c1-0.7 2-0.8 3-0.6c1.3 0.4 1.9 1.2 2.8 1.9 c0.5-1.3 1.1-2.6 2-3.8c0.9-1 2-1.7 3.3-1.8C20 4.8 21 5.4 21 6.3C21 6.7 20.8 7.5 20 7.5z"/></svg></div>\
An error occurred while loading the web debug toolbar. <a href="{{ url("_profiler_home")|escape('js') }}' + newToken + '">Open the web profiler.</a>\
</div>\
';
sfwdt.setAttribute('class', 'sf-toolbar sf-error-toolbar');
}
},
options
);
return this;
},
toggle: function(selector, elOn, elOff) {
var tmp = elOn.style.display,
el = document.getElementById(selector);
elOn.style.display = elOff.style.display;
elOff.style.display = tmp;
if (el) {
el.style.display = 'none' === tmp ? 'none' : 'block';
}
return this;
},
};
})();
}
Sfjs.loadToolbar('{{ token }}');
/*]]>*/</script>
<!-- END of Symfony Web Debug Toolbar -->

View File

@@ -0,0 +1,54 @@
{% extends '@WebProfiler/Profiler/base.html.twig' %}
{% block title 'Redirection Intercepted' %}
{% block head %}
{{ parent() }}
<style>
.sf-redirection-details {
background: var(--page-background);
box-shadow: inset 0 0 0 1px var(--menu-border-color), 0 0 0 5px var(--page-background);
border-radius: 6px;
margin: 45px auto 30px;
max-width: 800px;
padding: 30px 45px;
}
.sf-redirection-details h1 {
font-size: 21px;
font-weight: bold;
margin: 0 0 10px;
}
.sf-redirection-details p {
margin-top: 0;
}
.sf-redirection-details .sf-redirection-help {
color: var(--color-muted);
font-size: 14px;
margin: 45px 0 0;
}
</style>
{% endblock %}
{% block body %}
<div class="container">
{{ include('@WebProfiler/Profiler/header.html.twig', with_context = false) }}
<div class="sf-reset sf-redirection-details">
<div class="block-exception">
<h1>Redirection Intercepted</h1>
{% set absolute_url = absolute_url(location) %}
<p>This request redirects to <strong>{{ absolute_url }}</strong></p>
<p><a class="btn" href="{{ absolute_url }}">Follow redirect</a></p>
<p class="sf-redirection-help">
The redirect was intercepted by the Symfony Web Debug toolbar to help debugging.
For more information, see the "intercept-redirects" option of the Profiler.
</p>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,71 @@
<h2>Routing</h2>
<div class="metrics">
<div class="metric">
<span class="value">{{ request.route ?: '(none)' }}</span>
<span class="label">Matched route</span>
</div>
</div>
{% if request.route %}
<h3>Route Parameters</h3>
{% if request.routeParams is empty %}
<div class="empty">
<p>No parameters.</p>
</div>
{% else %}
{{ include('@WebProfiler/Profiler/table.html.twig', { data: request.routeParams, labels: ['Name', 'Value'] }, with_context = false) }}
{% endif %}
{% endif %}
{% if router.redirect %}
<h3>Route Redirection</h3>
<p>This page redirects to:</p>
<div class="card break-long-words">
{{ router.targetUrl }}
{% if router.targetRoute %}<span class="text-muted">(route: "{{ router.targetRoute }}")</span>{% endif %}
</div>
{% endif %}
<h3>Route Matching Logs</h3>
<div class="card">
<strong>Path to match:</strong> <code>{{ request.pathinfo }}</code>
</div>
<table id="router-logs">
<thead>
<tr>
<th>#</th>
<th>Route name</th>
<th>Path</th>
<th>Log</th>
</tr>
</thead>
<tbody>
{% for trace in traces %}
<tr class="{{ trace.level == 1 ? 'status-warning' : trace.level == 2 ? 'status-success' }}">
<td class="font-normal text-muted nowrap">{{ loop.index }}</td>
<td class="break-long-words">{{ trace.name }}</td>
<td class="break-long-words">{{ trace.path }}</td>
<td class="break-long-words font-normal">
{% if trace.level == 1 %}
Path almost matches, but
<span class="newline">{{ trace.log }}</span>
{% elseif trace.level == 2 %}
{{ trace.log }}
{% else %}
Path does not match
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<p class="help">
Note: These matching logs are based on the current router configuration,
which might differ from the configuration used when profiling this request.
</p>

File diff suppressed because one or more lines are too long