web interface for table_definition improved

This commit is contained in:
Priec
2026-08-04 17:20:58 +02:00
parent c2002c7da3
commit 803207b1af
20 changed files with 2989 additions and 612 deletions

View File

@@ -63,7 +63,7 @@
<h2>Add a column</h2>
<div class="form-grid">
<label>Column name
<input name="column_name_input" value="{{ page.draft.column_name_input }}" placeholder="number">
<input name="column_name_input" value="{{ page.draft.columns.name_input }}" placeholder="number">
</label>
<label>Column type
<select name="column_type_input" hx-post="/admin/tables/builder" hx-trigger="change"
@@ -71,56 +71,69 @@
hx-vals='{"action": "refresh"}'>
<option value="">Choose a type</option>
{% for column_type in column_types %}
<option value="{{ column_type }}" {% if page.draft.column_type_input == *column_type %}selected{% endif %}>{{ column_type }}</option>
<option value="{{ column_type }}" {% if page.draft.columns.type_input == *column_type %}selected{% endif %}>{{ column_type }}</option>
{% endfor %}
</select>
</label>
{% if page.draft.show_temporal_type() %}
{% if page.draft.columns.show_temporal_type() %}
<label>Temporal type
<select name="temporal_type_input">
<option value="">Choose a temporal type</option>
{% for temporal_type in temporal_types %}
<option value="{{ temporal_type }}" {% if page.draft.temporal_type_input == *temporal_type %}selected{% endif %}>{{ temporal_type }}</option>
<option value="{{ temporal_type }}" {% if page.draft.columns.temporal_type_input == *temporal_type %}selected{% endif %}>{{ temporal_type }}</option>
{% endfor %}
</select>
</label>
{% endif %}
{% if page.draft.show_gtin_type() %}
{% if page.draft.columns.show_gtin_type() %}
<label>GTIN type
<select name="gtin_type_input">
<option value="">Choose a GTIN length</option>
{% for gtin_type in gtin_types %}
<option value="{{ gtin_type }}" {% if page.draft.gtin_type_input == *gtin_type %}selected{% endif %}>GTIN-{{ gtin_type }}</option>
<option value="{{ gtin_type }}" {% if page.draft.columns.gtin_type_input == *gtin_type %}selected{% endif %}>GTIN-{{ gtin_type }}</option>
{% endfor %}
</select>
</label>
{% endif %}
{% if page.draft.show_money_options() %}
{% if page.draft.columns.show_decimal_arguments() %}
<label>Precision
<input name="decimal_precision_input" value="{{ page.draft.columns.decimal_precision_input }}"
inputmode="numeric" placeholder="12">
<small>Total digits stored, at least 1.</small>
</label>
<label>Scale
<input name="decimal_scale_input" value="{{ page.draft.columns.decimal_scale_input }}"
inputmode="numeric" placeholder="3">
<small>Digits after the point, no more than the precision.</small>
</label>
{% endif %}
{% if page.draft.columns.show_money_options() %}
<label>Currency
<input name="column_currency_input" value="{{ page.draft.column_currency_input }}" list="currency-codes"
<input name="column_currency_input" value="{{ page.draft.columns.currency_input }}" list="currency-codes"
maxlength="3" placeholder="EUR">
</label>
<label>Rounding
<select name="column_rounding_input">
<option value="none" {% if page.draft.column_rounding_input != "half-up" %}selected{% endif %}>none</option>
<option value="half-up" {% if page.draft.column_rounding_input == "half-up" %}selected{% endif %}>half-up</option>
<option value="none" {% if page.draft.columns.rounding_input != "half-up" %}selected{% endif %}>none</option>
<option value="half-up" {% if page.draft.columns.rounding_input == "half-up" %}selected{% endif %}>half-up</option>
</select>
</label>
{% endif %}
<label>Indexing
<select name="column_indexing_input">
<option value="no" {% if page.draft.column_indexing_input != "yes" %}selected{% endif %}>no</option>
<option value="yes" {% if page.draft.column_indexing_input == "yes" %}selected{% endif %}>yes</option>
<option value="no" {% if page.draft.columns.indexing_input != "yes" %}selected{% endif %}>no</option>
<option value="yes" {% if page.draft.columns.indexing_input == "yes" %}selected{% endif %}>yes</option>
</select>
</label>
<label>Quantity ledger
<select name="column_quantity_ledger_input">
<option value="no" {% if page.draft.column_quantity_ledger_input != "yes" %}selected{% endif %}>no</option>
<option value="yes" {% if page.draft.column_quantity_ledger_input == "yes" %}selected{% endif %}>yes</option>
<option value="no" {% if page.draft.columns.quantity_ledger_input != "yes" %}selected{% endif %}>no</option>
<option value="yes" {% if page.draft.columns.quantity_ledger_input == "yes" %}selected{% endif %}>yes</option>
</select>
<small>INT, BIGINT, DECIMAL or MONEY only.</small>
</label>
@@ -130,14 +143,14 @@
</section>
<section class="builder-section">
<h2>Columns <span class="count">{{ page.draft.columns.len() }}</span></h2>
<h2>Columns <span class="count">{{ page.draft.columns.added.len() }}</span></h2>
{% if page.draft.columns.is_empty() %}
<p class="hint">No columns yet. Describe one above and press <em>Add column</em>.</p>
{% else %}
<table class="builder-table">
<thead><tr><th>Name</th><th>Type</th><th>Indexed</th><th>Options</th><th></th></tr></thead>
<tbody>
{% for column in page.draft.columns %}
{% for column in page.draft.columns.added %}
<tr>
<td><code>{{ column.name }}</code></td>
<td>{{ column.data_type }}</td>
@@ -164,7 +177,7 @@
{% endif %}
{# The draft itself: one set of fields per column, in order. #}
{% for column in page.draft.columns %}
{% for column in page.draft.columns.added %}
<input type="hidden" name="column_names" value="{{ column.name }}">
<input type="hidden" name="column_types" value="{{ column.data_type }}">
<input type="hidden" name="column_indexed" value="{% if column.indexed %}yes{% else %}no{% endif %}">

View File

@@ -12,6 +12,7 @@
<p>Browse profiles, tables, and their physical columns.</p>
</div>
<div class="actions">
<a href="/admin/table-definition">Table definition</a>
<a href="/admin/tables/new">Add table</a>
<a href="/admin/logic/new">Add logic</a>
<a href="/admin/validation/new">Add validation</a>

View File

@@ -0,0 +1,155 @@
{#
The append-columns panel —
crate::pages::admin::table_definition::ui::ColumnPanelFragment, and what
workspace.html embeds inside #column-form.
Staging a column writes nothing, so these buttons swap this panel alone.
Saving is the form's own submit, which swaps the whole workspace. The field
names are the ones crate::schema::ColumnForm declares — the same set the
Add-table builder posts, decoded by the same code.
#}
{#
Staging swaps this panel and nothing else, so a refused column has nowhere
else to be reported. When the workspace embeds the panel it reports the
outcome at the top instead, and this block stays out of the way.
#}
{% import "ui/alert.html" as panel_alert %}
{% import "ui/dialog.html" as panel_dialog %}
{% if standalone_column_panel %}
{%- if let Some(message) = page.error %}{% call panel_alert::error("Could not add the column", message) %}{% endcall %}{% endif -%}
{%- if let Some(message) = page.status %}{% call panel_alert::success("Staged", message) %}{% endcall %}{% endif -%}
{%- if let Some(message) = page.error %}{% call panel_dialog::error("Could not add the column", message) %}{% endcall %}{% endif -%}
{% endif %}
<div class="form-grid">
<label>Column name
<input name="column_name_input" value="{{ page.columns.name_input }}" placeholder="issued_on">
</label>
<label>Column type
<select name="column_type_input"
hx-post="/admin/table-definition/columns/builder{{ page.selection.query() }}"
hx-trigger="change" hx-include="#column-form"
hx-target="#column-panel" hx-swap="innerHTML"
hx-vals='{"action": "refresh"}'>
<option value="">Choose a type</option>
{% for column_type in column_types %}
<option value="{{ column_type }}" {% if page.columns.type_input == *column_type %}selected{% endif %}>{{ column_type }}</option>
{% endfor %}
</select>
</label>
{% if page.columns.show_temporal_type() %}
<label>Temporal type
<select name="temporal_type_input">
<option value="">Choose a temporal type</option>
{% for temporal_type in temporal_types %}
<option value="{{ temporal_type }}" {% if page.columns.temporal_type_input == *temporal_type %}selected{% endif %}>{{ temporal_type }}</option>
{% endfor %}
</select>
</label>
{% endif %}
{% if page.columns.show_gtin_type() %}
<label>GTIN type
<select name="gtin_type_input">
<option value="">Choose a GTIN length</option>
{% for gtin_type in gtin_types %}
<option value="{{ gtin_type }}" {% if page.columns.gtin_type_input == *gtin_type %}selected{% endif %}>GTIN-{{ gtin_type }}</option>
{% endfor %}
</select>
</label>
{% endif %}
{% if page.columns.show_decimal_arguments() %}
<label>Precision
<input name="decimal_precision_input" value="{{ page.columns.decimal_precision_input }}"
inputmode="numeric" placeholder="12">
<small>Total digits stored, at least 1.</small>
</label>
<label>Scale
<input name="decimal_scale_input" value="{{ page.columns.decimal_scale_input }}"
inputmode="numeric" placeholder="3">
<small>Digits after the point, no more than the precision.</small>
</label>
{% endif %}
{% if page.columns.show_money_options() %}
<label>Currency
<input name="column_currency_input" value="{{ page.columns.currency_input }}" list="currency-codes"
maxlength="3" placeholder="EUR">
</label>
<label>Rounding
<select name="column_rounding_input">
<option value="none" {% if page.columns.rounding_input != "half-up" %}selected{% endif %}>none</option>
<option value="half-up" {% if page.columns.rounding_input == "half-up" %}selected{% endif %}>half-up</option>
</select>
</label>
{% endif %}
<label>Indexing
<select name="column_indexing_input">
<option value="no" {% if page.columns.indexing_input != "yes" %}selected{% endif %}>no</option>
<option value="yes" {% if page.columns.indexing_input == "yes" %}selected{% endif %}>yes</option>
</select>
</label>
<label>Quantity ledger
<select name="column_quantity_ledger_input">
<option value="no" {% if page.columns.quantity_ledger_input != "yes" %}selected{% endif %}>no</option>
<option value="yes" {% if page.columns.quantity_ledger_input == "yes" %}selected{% endif %}>yes</option>
</select>
<small>INT, BIGINT, DECIMAL or MONEY only.</small>
</label>
</div>
<button type="button" class="secondary"
hx-post="/admin/table-definition/columns/builder{{ page.selection.query() }}"
hx-include="#column-form" hx-target="#column-panel" hx-swap="innerHTML"
hx-vals='{"action": "add-column"}'>Stage column</button>
{% if page.columns.added.is_empty() %}
<p class="hint">No columns staged yet. Describe one above and press <em>Stage column</em>.</p>
{% else %}
<table class="builder-table">
<thead><tr><th>Name</th><th>Type</th><th>Indexed</th><th>Options</th><th></th></tr></thead>
<tbody>
{% for column in page.columns.added %}
<tr>
<td><code>{{ column.name }}</code></td>
<td>{{ column.data_type }}</td>
<td>
<button type="button" class="toggle"
hx-post="/admin/table-definition/columns/builder{{ page.selection.query() }}"
hx-include="#column-form" hx-target="#column-panel" hx-swap="innerHTML"
hx-vals='{"action": "toggle-index", "index": "{{ loop.index0 }}"}'>
{% if column.indexed %}[x]{% else %}[ ]{% endif %}
</button>
</td>
<td>
{% if column.quantity_ledger %}<span class="tag">quantity ledger</span>{% endif %}
{% if !column.option_label().is_empty() %}<span class="tag">{{ column.option_label() }}</span>{% endif %}
</td>
<td>
<button type="button" class="danger"
hx-post="/admin/table-definition/columns/builder{{ page.selection.query() }}"
hx-include="#column-form" hx-target="#column-panel" hx-swap="innerHTML"
hx-vals='{"action": "remove-column", "index": "{{ loop.index0 }}"}'>Remove</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{# The staged columns themselves: one set of fields per column, in order. #}
{% for column in page.columns.added %}
<input type="hidden" name="column_names" value="{{ column.name }}">
<input type="hidden" name="column_types" value="{{ column.data_type }}">
<input type="hidden" name="column_indexed" value="{% if column.indexed %}yes{% else %}no{% endif %}">
<input type="hidden" name="column_quantity_ledger" value="{% if column.quantity_ledger %}yes{% else %}no{% endif %}">
<input type="hidden" name="column_rounding" value="{{ column.money_mode.label() }}">
<input type="hidden" name="column_currencies" value="{{ column.currency }}">
{% endfor %}
<div class="form-actions">
<button type="submit">Add {{ page.columns.added.len() }} column(s) to the table</button>
</div>

View File

@@ -0,0 +1,32 @@
{# GET /admin/table-definition — crate::pages::admin::table_definition::ui::TableDefinitionPage #}
{% extends "ui/base.html" %}
{% block title %}Table definition{% endblock %}
{% block content %}
<main>
<section class="heading">
<div>
<p class="eyebrow">Table definition</p>
<h1>Table definition</h1>
<p>Pick a profile and a table, then change its definition.</p>
</div>
<div class="actions">
<a href="/admin">← Admin panel</a>
</div>
</section>
{#
One swap target for the whole workspace. Every write answers with this
markup re-read from the backend, so the screen after a change is the
definition as it now is, not the form that was submitted.
#}
<div id="table-definition-workspace" aria-live="polite">
{% include "pages/admin/table_definition/workspace.html" %}
</div>
</main>
<datalist id="currency-codes">
{% for code in currency_codes %}<option value="{{ code }}"></option>{% endfor %}
</datalist>
{% endblock %}

View File

@@ -0,0 +1,303 @@
{#
The whole workspace — crate::pages::admin::table_definition::ui::WorkspaceFragment,
and what table_definition.html embeds on first load.
Which panels appear follows how much has been selected: the copy and invoice
template panels need a profile, everything else needs a table, and a
backend-managed ("system") table gets none of the write panels at all,
because the server refuses every one of them for it.
#}
{% import "ui/alert.html" as alert %}
{% import "ui/dialog.html" as dialog %}
{%- if let Some(message) = page.error %}{% call alert::error("Could not continue", message) %}{% endcall %}{% endif -%}
{%- if let Some(message) = page.status %}{% call alert::success("Done", message) %}{% endcall %}{% endif -%}
{%- if let Some(message) = page.error %}{% call dialog::error("Could not continue", message) %}{% endcall %}{% endif -%}
{% if let Some(sql) = page.sql %}
{% if !sql.is_empty() %}
<section class="panel">
<h2>SQL the backend ran</h2>
<pre class="sql-preview">{{ sql }}</pre>
</section>
{% endif %}
{% endif %}
{% if !page.generated.is_empty() %}
<section class="panel">
<h2>Tables created from the template</h2>
<table class="builder-table">
<thead><tr><th>Table</th><th>Collection</th><th>Parent</th></tr></thead>
<tbody>
{% for generated in page.generated %}
<tr>
<td><code>{{ generated.table_name }}</code></td>
<td>{% if generated.collection_path.is_empty() %}<span class="hint">root</span>{% else %}<code>{{ generated.collection_path }}</code>{% endif %}</td>
<td>{% if generated.parent_table_name.is_empty() %}<span class="hint"></span>{% else %}<code>{{ generated.parent_table_name }}</code>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
{% endif %}
<section class="panel">
<h2>Selection</h2>
<div class="form-grid">
<label>Profile
<select name="profile" hx-get="/admin/table-definition/workspace"
hx-target="#table-definition-workspace" hx-swap="innerHTML">
<option value="">Choose a profile</option>
{% for profile in page.profiles %}
<option value="{{ profile }}" {% if page.selection.profile == *profile %}selected{% endif %}>{{ profile }}</option>
{% endfor %}
</select>
</label>
{% if page.selection.has_profile() %}
<label>Table
<select name="table" hx-get="/admin/table-definition/workspace"
hx-target="#table-definition-workspace" hx-swap="innerHTML"
hx-include="[name='profile']">
<option value="">Choose a table</option>
{% for table in page.tables %}
<option value="{{ table.name }}" {% if page.selection.table == table.name %}selected{% endif %}>
{{ table.name }}{% if table.is_system() %} (system){% endif %}
</option>
{% endfor %}
</select>
</label>
{% endif %}
</div>
{% if page.selection.has_profile() %}
<div class="actions panel-actions">
<a href="/admin/tables/new?profile={{ page.selection.profile }}">+ Create a table in this profile</a>
</div>
{% if page.tables.is_empty() %}
<p class="hint">This profile has no tables yet.</p>
{% else %}
<table class="builder-table">
<thead><tr><th>Table</th><th>Kind</th><th>Depends on</th><th>Row display</th></tr></thead>
<tbody>
{% for table in page.tables %}
<tr>
<td><code>{{ table.name }}</code></td>
<td>{{ table.table_kind }}</td>
<td>{% if table.depends_on.is_empty() %}<span class="hint"></span>{% else %}{{ table.depends_on|join(", ") }}{% endif %}</td>
<td>{% if table.row_display_columns.is_empty() %}<span class="hint">id</span>{% else %}{{ table.row_display_columns|join(", ") }}{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% else %}
<p class="hint">Nothing is selected yet. Every panel below acts on one profile, and most of them on one table inside it.</p>
{% endif %}
</section>
{% if let Some(detail) = page.detail %}
<section class="panel">
<h2>{{ detail.name }} <span class="count">{{ detail.columns.len() }} columns</span></h2>
<p class="hint">
Identified by
{%- if detail.row_display_columns.is_empty() %} its id
{%- else %} {{ detail.row_display_columns|join(", ") }}{% endif -%}
{%- if let Some(summary) = page.selected_table() -%}
{%- if !summary.depends_on.is_empty() %} · depends on {{ summary.depends_on|join(", ") }}{% endif -%}
{%- endif -%}
.
</p>
{% if detail.is_system() %}
<p class="hint">This table is backend-managed. Its definition is shown here, but it can only be changed through the backend's own APIs.</p>
{% endif %}
<table class="builder-table">
<thead><tr><th>Column</th><th>Type</th><th>Notes</th></tr></thead>
<tbody>
{% for column in detail.columns %}
<tr>
<td><code>{{ column.name }}</code></td>
<td>{{ column.field_type }}</td>
<td>
{%- for flag in column.flags() %}<span class="tag">{{ flag }}</span>{% endfor -%}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if !detail.scripts.is_empty() %}
<h3 class="panel-subhead">Scripts</h3>
<table class="builder-table">
<thead><tr><th>Target column</th><th>Type</th><th>Description</th></tr></thead>
<tbody>
{% for script in detail.scripts %}
<tr>
<td><code>{{ script.target_column }}</code></td>
<td>{{ script.target_column_type }}</td>
<td>
{{ script.description }}
<details><summary>source</summary><pre class="sql-preview">{{ script.script }}</pre></details>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</section>
{% endif %}
{% if page.table_is_writable() %}
{#
Append columns. The panel stages columns without writing anything; the
selection travels in the URL so the posted fields are exactly the ones the
Add-table builder posts, and both are decoded by the same code.
#}
<section class="panel">
<h2>Add columns to <code>{{ page.selection.table }}</code></h2>
<p class="hint">Columns are appended. Nothing that already exists is changed, and the new columns can be indexed as they are added.</p>
<form id="column-form"
hx-post="/admin/table-definition/columns{{ page.selection.query() }}"
hx-target="#table-definition-workspace" hx-swap="innerHTML"
hx-disabled-elt="button[type=submit]">
<div id="column-panel">
{% include "pages/admin/table_definition/column_panel.html" %}
</div>
</form>
</section>
{% if let Some(detail) = page.detail %}
<section class="panel">
<h2>Rename a column</h2>
<p class="hint">Renames what the column is called, not the physical column underneath, so stored data and scripts are untouched. Only possible while the table has no rows.</p>
<form hx-post="/admin/table-definition/rename"
hx-target="#table-definition-workspace" hx-swap="innerHTML">
<input type="hidden" name="profile" value="{{ page.selection.profile }}">
<input type="hidden" name="table" value="{{ page.selection.table }}">
<div class="form-grid">
<label>Column
<select name="old_column_name">
<option value="">Choose a column</option>
{% for column in detail.renameable_columns() %}
<option value="{{ column.name }}" {% if page.rename.old_column_name == column.name %}selected{% endif %}>{{ column.name }}</option>
{% endfor %}
</select>
</label>
<label>New name
<input name="new_column_name" value="{{ page.rename.new_column_name }}" placeholder="invoice_number">
</label>
</div>
<div class="form-actions">
<button type="submit">Rename column</button>
</div>
</form>
</section>
{% endif %}
<section class="panel danger-panel">
<h2>Delete <code>{{ page.selection.table }}</code></h2>
<p class="hint">
Drops the table and its definition, and the profile too when this was its
last table. The backend refuses to delete a table that still has rows.
</p>
<form hx-post="/admin/table-definition/delete"
hx-target="#table-definition-workspace" hx-swap="innerHTML">
<input type="hidden" name="profile" value="{{ page.selection.profile }}">
<input type="hidden" name="table" value="{{ page.selection.table }}">
<div class="form-grid">
<label class="wide">Type <code>{{ page.selection.table }}</code> to confirm
<input name="confirm_table_name" value="" autocomplete="off" placeholder="{{ page.selection.table }}">
</label>
</div>
<div class="form-actions">
<button type="submit" class="danger-submit">Delete table</button>
</div>
</form>
</section>
{% endif %}
{% if page.selection.has_profile() %}
<section class="panel">
<h2>Copy <code>{{ page.selection.profile }}</code> into a new profile</h2>
<p class="hint">Copies structure — tables, links and scripts — and no rows. Leave every table unticked to copy the whole profile.</p>
<form hx-post="/admin/table-definition/copy"
hx-target="#table-definition-workspace" hx-swap="innerHTML">
<input type="hidden" name="profile" value="{{ page.selection.profile }}">
<div class="form-grid">
<label>New profile name
<input name="target_profile_name" value="{{ page.copy.target_profile_name }}" placeholder="billing_2027">
</label>
</div>
{% if !page.copy_candidates().is_empty() %}
<div class="check-group">
{% for table in page.copy_candidates() %}
<label class="check">
<input type="checkbox" name="table_names" value="{{ table.name }}"
{% if page.copy_selected(table.name.as_str()) %}checked{% endif %}>
{{ table.name }}
</label>
{% endfor %}
</div>
{% endif %}
<div class="form-actions">
<button type="submit">Copy profile</button>
</div>
</form>
</section>
<section class="panel">
<h2>Create tables from an invoice template</h2>
<p class="hint">
Reads the <code>#let komp_ac_fields = (…)</code> contract out of a Typst
template. A path that matches an existing table and column becomes a
reference to it; anything else becomes a TEXT column to refine later, and
every <code>[]</code> becomes a child table.
</p>
<form hx-post="/admin/table-definition/invoice-template"
hx-target="#table-definition-workspace" hx-swap="innerHTML"
hx-disabled-elt="button[type=submit]">
<input type="hidden" name="profile" value="{{ page.selection.profile }}">
<div class="form-grid">
<label>Table name
<input name="table_name" value="{{ page.invoice.table_name }}" placeholder="invoice">
</label>
<label>Row display columns
<input name="row_display_columns" value="{{ page.invoice.row_display_columns }}" placeholder="number, issued_on">
<small>Comma-separated, and only for the root table.</small>
</label>
<label class="wide">Template source
<textarea name="typst_source" rows="10" placeholder="#let komp_ac_fields = (&#10; &quot;sidlo.nazov&quot;,&#10; &quot;people[].name&quot;,&#10;)">{{ page.invoice.typst_source }}</textarea>
</label>
</div>
<div class="form-actions">
<button type="submit">Create from template</button>
</div>
</form>
</section>
<section class="panel">
<h2>Column rename history</h2>
{% if page.history.is_empty() %}
<p class="hint">
No column in {% if page.selection.has_table() %}<code>{{ page.selection.table }}</code>{% else %}this profile{% endif %} has been renamed.
</p>
{% else %}
<table class="builder-table">
<thead><tr><th>Table</th><th>Was</th><th>Is</th><th>When</th></tr></thead>
<tbody>
{% for entry in page.history %}
<tr>
<td><code>{{ entry.table_name }}</code></td>
<td><code>{{ entry.old_column_name }}</code></td>
<td><code>{{ entry.new_column_name }}</code></td>
<td>{{ entry.created_at }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</section>
{% endif %}