209 lines
9.7 KiB
Twig
209 lines
9.7 KiB
Twig
{% extends 'base.html.twig' %}
|
|
{% import '_sidebar_tables.html.twig' as macros %}
|
|
|
|
{% block title %}{{ tableName }} — Query · Jormun Admin{% endblock %}
|
|
|
|
{% block breadcrumbs %}
|
|
<a href="{{ path('dashboard') }}"><i class="bi bi-house-fill"></i> Home</a>
|
|
<span class="sep">/</span>
|
|
<a href="{{ path('table_browse', {name: tableName}) }}">{{ tableName }}</a>
|
|
<span class="sep">/</span>
|
|
<span class="current">Query</span>
|
|
{% endblock %}
|
|
|
|
{% block sidebar_tables %}
|
|
{{ macros.sidebar_table_links(tables, tableName) }}
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<nav class="op-tabs">
|
|
<a href="{{ path('table_browse', {name: tableName}) }}" class="op-tab">
|
|
<i class="bi bi-grid-3x3-gap"></i> Browse
|
|
</a>
|
|
<a href="{{ path('table_structure', {name: tableName}) }}" class="op-tab">
|
|
<i class="bi bi-diagram-3"></i> Structure
|
|
</a>
|
|
<a href="{{ path('table_query', {name: tableName}) }}" class="op-tab active">
|
|
<i class="bi bi-search"></i> Query
|
|
</a>
|
|
<a href="{{ path('table_item_new', {name: tableName}) }}" class="op-tab">
|
|
<i class="bi bi-plus-circle"></i> Insert
|
|
</a>
|
|
</nav>
|
|
|
|
<div x-data="queryBuilder()" class="row g-3">
|
|
|
|
{# Builder form #}
|
|
<div class="col-12 col-lg-4">
|
|
<div class="content-card">
|
|
<div class="content-card-header">
|
|
<h6><i class="bi bi-funnel me-1" style="color:var(--jormun-teal);"></i> Query Builder</h6>
|
|
</div>
|
|
<div style="padding:1rem;">
|
|
<form method="POST" action="{{ path('table_query', {name: tableName}) }}">
|
|
|
|
{# Mode toggle #}
|
|
<div class="mb-3">
|
|
<label class="form-label" style="font-size:0.72rem;font-weight:500;text-transform:uppercase;letter-spacing:0.04em;color:#374151;">Mode</label>
|
|
<div class="d-flex gap-2">
|
|
<label style="display:flex;align-items:center;gap:0.3rem;font-size:0.78rem;cursor:pointer;">
|
|
<input type="radio" name="mode" value="query" x-model="queryMode" {% if mode != 'scan' %}checked{% endif %}> Query
|
|
</label>
|
|
<label style="display:flex;align-items:center;gap:0.3rem;font-size:0.78rem;cursor:pointer;">
|
|
<input type="radio" name="mode" value="scan" x-model="queryMode" {% if mode == 'scan' %}checked{% endif %}> Scan (full table)
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
{# Conditions (only for Query mode) #}
|
|
<div x-show="queryMode === 'query'">
|
|
<div class="mb-2" style="font-size:0.72rem;font-weight:500;text-transform:uppercase;letter-spacing:0.04em;color:#374151;">
|
|
Key Conditions
|
|
</div>
|
|
|
|
<div class="mb-2" style="font-size:0.72rem;color:#64748b;">
|
|
PK: <span class="type-badge">{{ keySchema.HASH.name ?? '?' }}</span>
|
|
{% if keySchema.RANGE is defined %}
|
|
SK: <span class="type-badge">{{ keySchema.RANGE.name }}</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<template x-for="(cond, i) in conditions" :key="i">
|
|
<div class="d-flex gap-1 mb-2 align-items-start">
|
|
<input
|
|
type="text"
|
|
:name="'cond_key[]'"
|
|
x-model="cond.key"
|
|
class="jormun-input"
|
|
placeholder="attribute"
|
|
style="width:35%;"
|
|
>
|
|
<select :name="'cond_op[]'" x-model="cond.op" class="jormun-select" style="width:28%;">
|
|
<option value="=">=</option>
|
|
<option value="<"><</option>
|
|
<option value="<="><=</option>
|
|
<option value=">">></option>
|
|
<option value=">=">>=</option>
|
|
<option value="begins_with">begins</option>
|
|
</select>
|
|
<input
|
|
type="text"
|
|
:name="'cond_val[]'"
|
|
x-model="cond.val"
|
|
class="jormun-input"
|
|
placeholder="value"
|
|
style="flex:1;"
|
|
>
|
|
<button type="button" @click="removeCond(i)" class="btn-danger-sm" style="flex-shrink:0;padding:0.35rem 0.5rem;">
|
|
<i class="bi bi-x"></i>
|
|
</button>
|
|
</div>
|
|
</template>
|
|
|
|
<button type="button" @click="addCond()" class="btn-jormun-outline w-100 mb-3" style="font-size:0.72rem;padding:0.3rem;">
|
|
<i class="bi bi-plus"></i> Add Condition
|
|
</button>
|
|
</div>
|
|
|
|
<button type="submit" class="btn-jormun w-100">
|
|
<i :class="queryMode === 'scan' ? 'bi bi-lightning' : 'bi bi-search'"></i>
|
|
<span x-text="queryMode === 'scan' ? 'Run Scan' : 'Run Query'"></span>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{# Results #}
|
|
<div class="col-12 col-lg-8">
|
|
{% if error %}
|
|
<div style="background:#fef2f2;border:1px solid #fca5a5;color:#991b1b;border-radius:6px;padding:0.75rem 1rem;margin-bottom:1rem;font-size:0.8rem;">
|
|
<i class="bi bi-exclamation-triangle-fill me-2"></i>{{ error }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if results is not null %}
|
|
<div class="content-card">
|
|
<div class="content-card-header">
|
|
<h6><i class="bi bi-table me-1" style="color:var(--jormun-teal);"></i> Results</h6>
|
|
<span class="stat-pill">{{ results|length }} item{{ results|length != 1 ? 's' : '' }}</span>
|
|
</div>
|
|
|
|
{% if results is empty %}
|
|
<div class="empty-state">
|
|
<i class="bi bi-search"></i>
|
|
<div>No items matched your query.</div>
|
|
</div>
|
|
{% else %}
|
|
{# Collect columns #}
|
|
{% set cols = {} %}
|
|
{% for item in results %}{% for k in item|keys %}{% set cols = cols|merge({(k): true}) %}{% endfor %}{% endfor %}
|
|
|
|
<div style="overflow-x:auto;">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
{% for col in cols|keys %}
|
|
<th>{{ col }}
|
|
{% if keySchema.HASH is defined and keySchema.HASH.name == col %}
|
|
<span class="type-badge">PK</span>
|
|
{% endif %}
|
|
{% if keySchema.RANGE is defined and keySchema.RANGE.name == col %}
|
|
<span class="type-badge">SK</span>
|
|
{% endif %}
|
|
</th>
|
|
{% endfor %}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for item in results %}
|
|
<tr>
|
|
{% for col in cols|keys %}
|
|
<td class="cell-value {% if keySchema.HASH is defined and keySchema.HASH.name == col %}pk-cell{% endif %}">
|
|
{% if item[col] is defined %}
|
|
{% set val = item[col] %}
|
|
{% if val is iterable %}
|
|
<span style="color:#6366f1;">{{ val|json_encode|slice(0, 50) }}{% if val|json_encode|length > 50 %}…{% endif %}</span>
|
|
{% else %}
|
|
{{ val|e|slice(0, 60) }}{% if (val|e|length) > 60 %}…{% endif %}
|
|
{% endif %}
|
|
{% else %}
|
|
<span class="null-val">NULL</span>
|
|
{% endif %}
|
|
</td>
|
|
{% endfor %}
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% else %}
|
|
<div class="content-card">
|
|
<div class="empty-state" style="padding:4rem 1rem;">
|
|
<i class="bi bi-search" style="color:#cbd5e1;"></i>
|
|
<div style="font-family:'Syne',sans-serif;font-weight:700;font-size:0.9rem;color:#374151;margin-bottom:0.3rem;">Build a query</div>
|
|
<div style="font-size:0.75rem;color:#94a3b8;">
|
|
Set conditions on the left and click Run Query, or use Scan for all items.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
function queryBuilder() {
|
|
return {
|
|
queryMode: '{{ mode }}',
|
|
conditions: [{ key: '{{ keySchema.HASH.name ?? '' }}', op: '=', val: '' }],
|
|
addCond() { this.conditions.push({ key: '', op: '=', val: '' }); },
|
|
removeCond(i) { if (this.conditions.length > 1) this.conditions.splice(i, 1); }
|
|
};
|
|
}
|
|
</script>
|
|
{% endblock %}
|