534 lines
27 KiB
Twig
534 lines
27 KiB
Twig
|
{% extends request.isXmlHttpRequest ? '@WebProfiler/Profiler/ajax_layout.html.twig' : '@WebProfiler/Profiler/layout.html.twig' %}
|
||
|
|
||
|
{% import _self as helper %}
|
||
|
|
||
|
{% block toolbar %}
|
||
|
{% if collector.querycount > 0 or collector.invalidEntityCount > 0 %}
|
||
|
|
||
|
{% set icon %}
|
||
|
{% set status = collector.invalidEntityCount > 0 ? 'red' : collector.querycount > 50 ? 'yellow' %}
|
||
|
|
||
|
{% if profiler_markup_version >= 3 %}
|
||
|
{{ include('@Doctrine/Collector/database.svg') }}
|
||
|
{% else %}
|
||
|
<span class="icon">{{ include('@Doctrine/Collector/icon.svg') }}</span>
|
||
|
{% endif %}
|
||
|
|
||
|
{% if collector.querycount == 0 and collector.invalidEntityCount > 0 %}
|
||
|
<span class="sf-toolbar-value">{{ collector.invalidEntityCount }}</span>
|
||
|
<span class="sf-toolbar-label">errors</span>
|
||
|
{% else %}
|
||
|
<span class="sf-toolbar-value">{{ collector.querycount }}</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.time * 1000) }}</span>
|
||
|
<span class="sf-toolbar-label">ms</span>
|
||
|
</span>
|
||
|
{% endif %}
|
||
|
{% endset %}
|
||
|
|
||
|
{% set text %}
|
||
|
<div class="sf-toolbar-info-piece">
|
||
|
<b>Database Queries</b>
|
||
|
<span class="sf-toolbar-status {{ collector.querycount > 50 ? 'sf-toolbar-status-yellow' : '' }}">{{ collector.querycount }}</span>
|
||
|
</div>
|
||
|
<div class="sf-toolbar-info-piece">
|
||
|
<b>Different statements</b>
|
||
|
<span class="sf-toolbar-status">{{ collector.groupedQueryCount }}</span>
|
||
|
</div>
|
||
|
<div class="sf-toolbar-info-piece">
|
||
|
<b>Query time</b>
|
||
|
<span>{{ '%0.2f'|format(collector.time * 1000) }} ms</span>
|
||
|
</div>
|
||
|
<div class="sf-toolbar-info-piece">
|
||
|
<b>Invalid entities</b>
|
||
|
<span class="sf-toolbar-status {{ collector.invalidEntityCount > 0 ? 'sf-toolbar-status-red' : '' }}">{{ collector.invalidEntityCount }}</span>
|
||
|
</div>
|
||
|
{% if collector.cacheEnabled %}
|
||
|
<div class="sf-toolbar-info-piece">
|
||
|
<b>Cache hits</b>
|
||
|
<span class="sf-toolbar-status sf-toolbar-status-green">{{ collector.cacheHitsCount }}</span>
|
||
|
</div>
|
||
|
<div class="sf-toolbar-info-piece">
|
||
|
<b>Cache misses</b>
|
||
|
<span class="sf-toolbar-status {{ collector.cacheMissesCount > 0 ? 'sf-toolbar-status-yellow' : '' }}">{{ collector.cacheMissesCount }}</span>
|
||
|
</div>
|
||
|
<div class="sf-toolbar-info-piece">
|
||
|
<b>Cache puts</b>
|
||
|
<span class="sf-toolbar-status {{ collector.cachePutsCount > 0 ? 'sf-toolbar-status-yellow' : '' }}">{{ collector.cachePutsCount }}</span>
|
||
|
</div>
|
||
|
{% else %}
|
||
|
<div class="sf-toolbar-info-piece">
|
||
|
<b>Second Level Cache</b>
|
||
|
<span class="sf-toolbar-status">disabled</span>
|
||
|
</div>
|
||
|
{% endif %}
|
||
|
{% endset %}
|
||
|
|
||
|
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status|default('') }) }}
|
||
|
|
||
|
{% endif %}
|
||
|
{% endblock %}
|
||
|
|
||
|
{% block menu %}
|
||
|
<span class="label {{ collector.invalidEntityCount > 0 ? 'label-status-error' }} {{ collector.querycount == 0 ? 'disabled' }}">
|
||
|
<span class="icon">{{ include('@Doctrine/Collector/' ~ (profiler_markup_version < 3 ? 'icon' : 'database') ~ '.svg') }}</span>
|
||
|
<strong>Doctrine</strong>
|
||
|
{% if collector.invalidEntityCount %}
|
||
|
<span class="count">
|
||
|
<span>{{ collector.invalidEntityCount }}</span>
|
||
|
</span>
|
||
|
{% endif %}
|
||
|
</span>
|
||
|
{% endblock %}
|
||
|
|
||
|
{% block panel %}
|
||
|
{% if 'explain' == page %}
|
||
|
{{ render(controller('Doctrine\\Bundle\\DoctrineBundle\\Controller\\ProfilerController::explainAction', {
|
||
|
token: token,
|
||
|
panel: 'db',
|
||
|
connectionName: request.query.get('connection'),
|
||
|
query: request.query.get('query')
|
||
|
})) }}
|
||
|
{% else %}
|
||
|
{{ block('queries') }}
|
||
|
{% endif %}
|
||
|
{% endblock %}
|
||
|
|
||
|
{% block queries %}
|
||
|
<style>
|
||
|
.time-container { position: relative; }
|
||
|
.time-container .nowrap { position: relative; z-index: 1; text-shadow: 0 0 2px #fff; }
|
||
|
.time-bar { display: block; position: absolute; top: 0; left: 0; bottom: 0; background: #e0e0e0; }
|
||
|
.sql-runnable.sf-toggle-content.sf-toggle-visible { display: flex; flex-direction: column; }
|
||
|
.sql-runnable button { align-self: end; }
|
||
|
{% if profiler_markup_version >= 3 %}
|
||
|
.highlight .keyword { color: var(--highlight-keyword); font-weight: bold; }
|
||
|
.highlight .word { color: var(--color-text); }
|
||
|
.highlight .variable { color: var(--highlight-variable); }
|
||
|
.highlight .symbol { color: var(--color-text); }
|
||
|
.highlight .comment { color: var(--highlight-comment); }
|
||
|
.highlight .string { color: var(--highlight-string); }
|
||
|
.highlight .number { color: var(--highlight-constant); font-weight: bold; }
|
||
|
.highlight .error { color: var(--highlight-error); }
|
||
|
{% endif %}
|
||
|
</style>
|
||
|
|
||
|
<h2>Query Metrics</h2>
|
||
|
|
||
|
<div class="metrics">
|
||
|
<div class="metric-group">
|
||
|
<div class="metric">
|
||
|
<span class="value">{{ collector.querycount }}</span>
|
||
|
<span class="label">Database Queries</span>
|
||
|
</div>
|
||
|
|
||
|
<div class="metric">
|
||
|
<span class="value">{{ collector.groupedQueryCount }}</span>
|
||
|
<span class="label">Different statements</span>
|
||
|
</div>
|
||
|
|
||
|
<div class="metric">
|
||
|
<span class="value">{{ '%0.2f'|format(collector.time * 1000) }} ms</span>
|
||
|
<span class="label">Query time</span>
|
||
|
</div>
|
||
|
|
||
|
<div class="metric">
|
||
|
<span class="value">{{ collector.invalidEntityCount }}</span>
|
||
|
<span class="label">Invalid entities</span>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
{% if collector.cacheEnabled %}
|
||
|
<div class="metric-group">
|
||
|
<div class="metric">
|
||
|
<span class="value">{{ collector.cacheHitsCount }}</span>
|
||
|
<span class="label">Cache hits</span>
|
||
|
</div>
|
||
|
<div class="metric">
|
||
|
<span class="value">{{ collector.cacheMissesCount }}</span>
|
||
|
<span class="label">Cache misses</span>
|
||
|
</div>
|
||
|
<div class="metric">
|
||
|
<span class="value">{{ collector.cachePutsCount }}</span>
|
||
|
<span class="label">Cache puts</span>
|
||
|
</div>
|
||
|
</div>
|
||
|
{% endif %}
|
||
|
</div>
|
||
|
|
||
|
<div class="sf-tabs" style="margin-top: 20px;">
|
||
|
<div class="tab {{ collector.queries is empty ? 'disabled' }}">
|
||
|
{% set group_queries = request.query.getBoolean('group') %}
|
||
|
<h3 class="tab-title">
|
||
|
{% if group_queries %}
|
||
|
Grouped Statements
|
||
|
{% else %}
|
||
|
Queries
|
||
|
{% endif %}
|
||
|
</h3>
|
||
|
|
||
|
<div class="tab-content">
|
||
|
{% if not collector.queries %}
|
||
|
<div class="empty">
|
||
|
<p>No executed queries.</p>
|
||
|
</div>
|
||
|
{% else %}
|
||
|
{% if group_queries %}
|
||
|
<p><a href="{{ path('_profiler', { panel: 'db', token: token }) }}">Show all queries</a></p>
|
||
|
{% else %}
|
||
|
<p><a href="{{ path('_profiler', { panel: 'db', token: token, group: true }) }}">Group similar statements</a></p>
|
||
|
{% endif %}
|
||
|
|
||
|
{% for connection, queries in collector.queries %}
|
||
|
{% if collector.connections|length > 1 %}
|
||
|
<h3>{{ connection }} <small>connection</small></h3>
|
||
|
{% endif %}
|
||
|
|
||
|
{% if queries is empty %}
|
||
|
<div class="empty">
|
||
|
<p>No database queries were performed.</p>
|
||
|
</div>
|
||
|
{% else %}
|
||
|
{% if group_queries %}
|
||
|
{% set queries = collector.groupedQueries[connection] %}
|
||
|
{% endif %}
|
||
|
<table class="alt queries-table">
|
||
|
<thead>
|
||
|
<tr>
|
||
|
{% if group_queries %}
|
||
|
<th class="nowrap" onclick="javascript:sortTable(this, 0, 'queries-{{ loop.index }}')" data-sort-direction="1" style="cursor: pointer;">Time<span class="text-muted">▼</span></th>
|
||
|
<th class="nowrap" onclick="javascript:sortTable(this, 1, 'queries-{{ loop.index }}')" style="cursor: pointer;">Count<span></span></th>
|
||
|
{% else %}
|
||
|
<th class="nowrap" onclick="javascript:sortTable(this, 0, 'queries-{{ loop.index }}')" data-sort-direction="-1" style="cursor: pointer;">#<span class="text-muted">▲</span></th>
|
||
|
<th class="nowrap" onclick="javascript:sortTable(this, 1, 'queries-{{ loop.index }}')" style="cursor: pointer;">Time<span></span></th>
|
||
|
{% endif %}
|
||
|
<th style="width: 100%;">Info</th>
|
||
|
</tr>
|
||
|
</thead>
|
||
|
<tbody id="queries-{{ loop.index }}">
|
||
|
{% for i, query in queries %}
|
||
|
{% set i = group_queries ? query.index : i %}
|
||
|
<tr id="queryNo-{{ i }}-{{ loop.parent.loop.index }}">
|
||
|
{% if group_queries %}
|
||
|
<td class="time-container">
|
||
|
<span class="time-bar" style="width:{{ '%0.2f'|format(query.executionPercent) }}%"></span>
|
||
|
<span class="nowrap">{{ '%0.2f'|format(query.executionMS * 1000) }} ms<br />({{ '%0.2f'|format(query.executionPercent) }}%)</span>
|
||
|
</td>
|
||
|
<td class="nowrap">{{ query.count }}</td>
|
||
|
{% else %}
|
||
|
<td class="nowrap">{{ loop.index }}</td>
|
||
|
<td class="nowrap">{{ '%0.2f'|format(query.executionMS * 1000) }} ms</td>
|
||
|
{% endif %}
|
||
|
<td>
|
||
|
{{ query.sql|doctrine_prettify_sql }}
|
||
|
|
||
|
<div>
|
||
|
<strong class="font-normal text-small">Parameters</strong>: {{ profiler_dump(query.params, 2) }}
|
||
|
</div>
|
||
|
|
||
|
<div class="text-small font-normal">
|
||
|
<a href="#" class="sf-toggle link-inverse" data-toggle-selector="#formatted-query-{{ i }}-{{ loop.parent.loop.index }}" data-toggle-alt-content="Hide formatted query">View formatted query</a>
|
||
|
|
||
|
{% if query.runnable %}
|
||
|
|
||
|
<a href="#" class="sf-toggle link-inverse" data-toggle-selector="#original-query-{{ i }}-{{ loop.parent.loop.index }}" data-toggle-alt-content="Hide runnable query">View runnable query</a>
|
||
|
{% endif %}
|
||
|
|
||
|
{% if query.explainable %}
|
||
|
|
||
|
<a class="link-inverse" href="{{ path('_profiler', { panel: 'db', token: token, page: 'explain', connection: connection, query: i }) }}" onclick="return explain(this);" data-target-id="explain-{{ i }}-{{ loop.parent.loop.index }}">Explain query</a>
|
||
|
{% endif %}
|
||
|
|
||
|
{% if query.backtrace is defined %}
|
||
|
|
||
|
<a href="#" class="sf-toggle link-inverse" data-toggle-selector="#backtrace-{{ i }}-{{ loop.parent.loop.index }}" data-toggle-alt-content="Hide query backtrace">View query backtrace</a>
|
||
|
{% endif %}
|
||
|
</div>
|
||
|
|
||
|
<div id="formatted-query-{{ i }}-{{ loop.parent.loop.index }}" class="sql-runnable hidden">
|
||
|
{{ query.sql|doctrine_format_sql(highlight = true) }}
|
||
|
<button class="btn btn-sm hidden" data-clipboard-text="{{ query.sql|doctrine_format_sql(highlight = false)|e('html_attr') }}">Copy</button>
|
||
|
</div>
|
||
|
|
||
|
{% if query.runnable %}
|
||
|
<div id="original-query-{{ i }}-{{ loop.parent.loop.index }}" class="sql-runnable hidden">
|
||
|
{% set runnable_sql = (query.sql ~ ';')|doctrine_replace_query_parameters(query.params) %}
|
||
|
{{ runnable_sql|doctrine_prettify_sql }}
|
||
|
<button class="btn btn-sm hidden" data-clipboard-text="{{ runnable_sql|e('html_attr') }}">Copy</button>
|
||
|
</div>
|
||
|
{% endif %}
|
||
|
|
||
|
{% if query.explainable %}
|
||
|
<div id="explain-{{ i }}-{{ loop.parent.loop.index }}" class="sql-explain"></div>
|
||
|
{% endif %}
|
||
|
|
||
|
{% if query.backtrace is defined %}
|
||
|
<div id="backtrace-{{ i }}-{{ loop.parent.loop.index }}" class="hidden">
|
||
|
<table>
|
||
|
<thead>
|
||
|
<tr>
|
||
|
<th scope="col">#</th>
|
||
|
<th scope="col">File/Call</th>
|
||
|
</tr>
|
||
|
</thead>
|
||
|
<tbody>
|
||
|
{% for trace in query.backtrace %}
|
||
|
<tr>
|
||
|
<td>{{ loop.index }}</td>
|
||
|
<td>
|
||
|
<span class="text-small">
|
||
|
{% set line_number = trace.line|default(1) %}
|
||
|
{% if trace.file is defined %}
|
||
|
<a href="{{ trace.file|file_link(line_number) }}">
|
||
|
{% endif %}
|
||
|
{{- trace.class|default ~ (trace.class is defined ? trace.type|default('::')) -}}
|
||
|
<span class="status-warning">{{ trace.function }}</span>
|
||
|
{% if trace.file is defined %}
|
||
|
</a>
|
||
|
{% endif %}
|
||
|
(line {{ line_number }})
|
||
|
</span>
|
||
|
</td>
|
||
|
</tr>
|
||
|
{% endfor %}
|
||
|
</tbody>
|
||
|
</table>
|
||
|
</div>
|
||
|
{% endif %}
|
||
|
</td>
|
||
|
</tr>
|
||
|
{% endfor %}
|
||
|
</tbody>
|
||
|
</table>
|
||
|
{% endif %}
|
||
|
{% endfor %}
|
||
|
{% endif %}
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div class="tab {{ collector.connections is empty ? 'disabled' }}">
|
||
|
<h3 class="tab-title">Database Connections</h3>
|
||
|
<div class="tab-content">
|
||
|
{% if not collector.connections %}
|
||
|
<div class="empty">
|
||
|
<p>There are no configured database connections.</p>
|
||
|
</div>
|
||
|
{% else %}
|
||
|
{{ helper.render_simple_table('Name', 'Service', collector.connections) }}
|
||
|
{% endif %}
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div class="tab {{ collector.managers is empty ? 'disabled' }}">
|
||
|
<h3 class="tab-title">Entity Managers</h3>
|
||
|
<div class="tab-content">
|
||
|
|
||
|
{% if not collector.managers %}
|
||
|
<div class="empty">
|
||
|
<p>There are no configured entity managers.</p>
|
||
|
</div>
|
||
|
{% else %}
|
||
|
{{ helper.render_simple_table('Name', 'Service', collector.managers) }}
|
||
|
{% endif %}
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div class="tab {{ not collector.cacheEnabled ? 'disabled' }}">
|
||
|
<h3 class="tab-title">Second Level Cache</h3>
|
||
|
<div class="tab-content">
|
||
|
|
||
|
{% if not collector.cacheEnabled %}
|
||
|
<div class="empty">
|
||
|
<p>Second Level Cache is not enabled.</p>
|
||
|
</div>
|
||
|
{% else %}
|
||
|
{% if not collector.cacheCounts %}
|
||
|
<div class="empty">
|
||
|
<p>Second level cache information is not available.</p>
|
||
|
</div>
|
||
|
{% else %}
|
||
|
<div class="metrics">
|
||
|
<div class="metric">
|
||
|
<span class="value">{{ collector.cacheCounts.hits }}</span>
|
||
|
<span class="label">Hits</span>
|
||
|
</div>
|
||
|
|
||
|
<div class="metric">
|
||
|
<span class="value">{{ collector.cacheCounts.misses }}</span>
|
||
|
<span class="label">Misses</span>
|
||
|
</div>
|
||
|
|
||
|
<div class="metric">
|
||
|
<span class="value">{{ collector.cacheCounts.puts }}</span>
|
||
|
<span class="label">Puts</span>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
{% if collector.cacheRegions.hits %}
|
||
|
<h3>Number of cache hits</h3>
|
||
|
{{ helper.render_simple_table('Region', 'Hits', collector.cacheRegions.hits) }}
|
||
|
{% endif %}
|
||
|
|
||
|
{% if collector.cacheRegions.misses %}
|
||
|
<h3>Number of cache misses</h3>
|
||
|
{{ helper.render_simple_table('Region', 'Misses', collector.cacheRegions.misses) }}
|
||
|
{% endif %}
|
||
|
|
||
|
{% if collector.cacheRegions.puts %}
|
||
|
<h3>Number of cache puts</h3>
|
||
|
{{ helper.render_simple_table('Region', 'Puts', collector.cacheRegions.puts) }}
|
||
|
{% endif %}
|
||
|
{% endif %}
|
||
|
{% endif %}
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div class="tab {{ not collector.entities ? 'disabled' }}">
|
||
|
<h3 class="tab-title">Entities Mapping</h3>
|
||
|
<div class="tab-content">
|
||
|
|
||
|
{% if not collector.entities %}
|
||
|
<div class="empty">
|
||
|
<p>No mapped entities.</p>
|
||
|
</div>
|
||
|
{% else %}
|
||
|
{% for manager, classes in collector.entities %}
|
||
|
{% if collector.managers|length > 1 %}
|
||
|
<h3>{{ manager }} <small>entity manager</small></h3>
|
||
|
{% endif %}
|
||
|
|
||
|
{% if classes is empty %}
|
||
|
<div class="empty">
|
||
|
<p>No loaded entities.</p>
|
||
|
</div>
|
||
|
{% else %}
|
||
|
<table>
|
||
|
<thead>
|
||
|
<tr>
|
||
|
<th scope="col">Class</th>
|
||
|
<th scope="col">Mapping errors</th>
|
||
|
</tr>
|
||
|
</thead>
|
||
|
<tbody>
|
||
|
{% for class in classes %}
|
||
|
{% set contains_errors = collector.mappingErrors[manager] is defined and collector.mappingErrors[manager][class.class] is defined %}
|
||
|
<tr class="{{ contains_errors ? 'status-error' }}">
|
||
|
<td>
|
||
|
<a href="{{ class.file|file_link(class.line) }}">{{ class. class}}</a>
|
||
|
</td>
|
||
|
<td class="font-normal">
|
||
|
{% if contains_errors %}
|
||
|
<ul>
|
||
|
{% for error in collector.mappingErrors[manager][class.class] %}
|
||
|
<li>{{ error }}</li>
|
||
|
{% endfor %}
|
||
|
</ul>
|
||
|
{% else %}
|
||
|
No errors.
|
||
|
{% endif %}
|
||
|
</td>
|
||
|
</tr>
|
||
|
{% endfor %}
|
||
|
</tbody>
|
||
|
</table>
|
||
|
{% endif %}
|
||
|
{% endfor %}
|
||
|
{% endif %}
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<script type="text/javascript">//<![CDATA[
|
||
|
function explain(link) {
|
||
|
"use strict";
|
||
|
|
||
|
var targetId = link.getAttribute('data-target-id');
|
||
|
var targetElement = document.getElementById(targetId);
|
||
|
|
||
|
if (targetElement.style.display != 'block') {
|
||
|
if (targetElement.getAttribute('data-sfurl') !== link.href) {
|
||
|
fetch(link.href, {
|
||
|
headers: {'X-Requested-With': 'XMLHttpRequest'}
|
||
|
}).then(async function (response) {
|
||
|
targetElement.innerHTML = await response.text()
|
||
|
targetElement.setAttribute('data-sfurl', link.href)
|
||
|
}, function () {
|
||
|
targetElement.innerHTML = 'An error occurred while loading the query explanation.';
|
||
|
})
|
||
|
}
|
||
|
|
||
|
targetElement.style.display = 'block';
|
||
|
link.innerHTML = 'Hide query explanation';
|
||
|
} else {
|
||
|
targetElement.style.display = 'none';
|
||
|
link.innerHTML = 'Explain query';
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
function sortTable(header, column, targetId) {
|
||
|
"use strict";
|
||
|
|
||
|
var direction = parseInt(header.getAttribute('data-sort-direction')) || 1,
|
||
|
items = [],
|
||
|
target = document.getElementById(targetId),
|
||
|
rows = target.children,
|
||
|
headers = header.parentElement.children,
|
||
|
i;
|
||
|
|
||
|
for (i = 0; i < rows.length; ++i) {
|
||
|
items.push(rows[i]);
|
||
|
}
|
||
|
|
||
|
for (i = 0; i < headers.length; ++i) {
|
||
|
headers[i].removeAttribute('data-sort-direction');
|
||
|
if (headers[i].children.length > 0) {
|
||
|
headers[i].children[0].innerHTML = '';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
header.setAttribute('data-sort-direction', (-1*direction).toString());
|
||
|
header.children[0].innerHTML = direction > 0 ? '<span class="text-muted">▲</span>' : '<span class="text-muted">▼</span>';
|
||
|
|
||
|
items.sort(function(a, b) {
|
||
|
return direction * (parseFloat(a.children[column].innerHTML) - parseFloat(b.children[column].innerHTML));
|
||
|
});
|
||
|
|
||
|
for (i = 0; i < items.length; ++i) {
|
||
|
target.appendChild(items[i]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (navigator.clipboard) {
|
||
|
document.querySelectorAll('[data-clipboard-text]').forEach(function(button) {
|
||
|
button.classList.remove('hidden');
|
||
|
button.addEventListener('click', function() {
|
||
|
navigator.clipboard.writeText(button.getAttribute('data-clipboard-text'));
|
||
|
})
|
||
|
});
|
||
|
}
|
||
|
|
||
|
//]]></script>
|
||
|
{% endblock %}
|
||
|
|
||
|
{% macro render_simple_table(label1, label2, data) %}
|
||
|
<table>
|
||
|
<thead>
|
||
|
<tr>
|
||
|
<th scope="col" class="key">{{ label1 }}</th>
|
||
|
<th scope="col">{{ label2 }}</th>
|
||
|
</tr>
|
||
|
</thead>
|
||
|
<tbody>
|
||
|
{% for key, value in data %}
|
||
|
<tr>
|
||
|
<th scope="row">{{ key }}</th>
|
||
|
<td>{{ value }}</td>
|
||
|
</tr>
|
||
|
{% endfor %}
|
||
|
</tbody>
|
||
|
</table>
|
||
|
{% endmacro %}
|