Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions .claude/memory/advanced_search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Advanced Search Feature

## Status: Implemented and working (v4)

## What was built
Multi-condition advanced search panel for the Rows tab. Accessible via an "Advanced" toggle button next to the existing Apply/× buttons.

### Features (v1)
- Multiple filter conditions (unlimited rows)
- "Advanced Filter Active" badge in pagination row when active
- Right-click "Filter Rows By Value" appends a pre-filled row when panel is open
- Resets on table switch and basic × reset button
- `#output` top offset recalculated when panel opens/closes

### Features added in v2
- **Per-row AND/OR connector pills** — each row after the first has AND/OR buttons; first row shows "WHERE" label; enables `(A) AND (B) OR (C)` style expressions
- **Show Query button** — toggles a `<pre>` box showing the full `SELECT * FROM "schema"."table" WHERE ...;` with a Copy button (reuses `copyToClipboard()`)
- **Expanded operator set** (18 operators, grouped with `<optgroup>`):
- Comparison: `=`, `<>`, `<`, `>`, `<=`, `>=`
- List: `IN` (comma-sep → `IN ('a','b')`), `NOT IN`
- Null: `IS NULL`, `IS NOT NULL`
- Range: `BETWEEN` (From/To inputs), `NOT BETWEEN`
- Pattern: Contains, Not contains, Has prefix, Has suffix + case-insensitive variants (ILIKE)
- Regex: Matches regex (`~`), Matches regex case insensitive (`~*`)
- **`getOpInputType(op)`** helper: returns `"none" | "single" | "list" | "range"` — controls which input variant is shown
- **`buildFullQuery()`** — builds full SELECT string using `getCurrentObject().name`

## Files changed
- `static/index.html` — Advanced button, `#advanced_search_panel`, `#adv_search_active_badge`, Show Query button, `#adv_query_display`
- `static/css/app.css` — appended styles for panel, connector pills, range inputs, query display box
- `static/js/app.js` — see key functions below

## Key JS functions (app.js)
- `var advancedSearchActive = false` — global flag
- `escapeSqlLiteral(val)` — doubles single-quotes (also applied to simple filter)
- `getOpInputType(op)` — returns input variant type for an operator
- `buildAdvancedSearchRow(isFirst)` — builds a condition row; `isFirst=true` → WHERE label, no pill
- `buildAdvancedWhereClause()` — iterates rows, reads `data-row-conj` per row, builds SQL
- `buildFullQuery()` — wraps WHERE clause in full SELECT statement
- `applyAdvancedSearch()` — stores WHERE in panel `.data("where")`, sets flag, reloads
- `resetAdvancedSearch()` — clears flag/badge, empties rows, adds `buildAdvancedSearchRow(true)`
- `adjustOutputTop()` — sets `#output` CSS top to `#pagination` outerHeight
- `bindAdvancedOpHandlers()` — delegated handler showing correct input variant per operator

## Bug fix (v4b): first row showed unnecessary − delete button
- `buildAdvancedSearchRow(isFirst)` now only appends the remove button when `isFirst=false`

## Added in v4: regex operators
- `"regex": "~ 'DATA'"` and `"iregex": "~* 'DATA'"` added to `filterOptions`
- Two new options appended to the Pattern `<optgroup>` in `buildAdvancedSearchRow()`
- No other changes needed — `getOpInputType()` returns `"single"` by default, `buildAdvancedWhereClause()` handles it unchanged

## Bug fix (v3): advanced panel obscuring table rows
- **Root cause**: `.with-pagination #output { top: 50px !important }` in `app.css` — the `!important` beat jQuery's inline style set by `adjustOutputTop()`
- **Fix 1**: removed `!important` from that CSS rule so JS inline style wins
- **Fix 2**: added `adjustOutputTop()` call immediately after `$("#body").prop("class", "with-pagination")` in `showTableContent()` — so offset is recalculated on every table load, not just on panel open/close

## Key JS edits (app.js)
- `showTableContent()` — advanced takes precedence over simple filter when `advancedSearchActive`; calls `adjustOutputTop()` after setting `with-pagination` class
- `buildTableFilters()` — syncs columns into existing advanced rows; passes `isFirst=true`
- Objects click handler — calls `resetAdvancedSearch()` on table switch
- `reset-filters` button — calls `resetAdvancedSearch()`
- `filter_by_value` context menu — appends pre-filled row when panel visible

## No backend changes needed
The existing `where` param on `GET /api/tables/:table/rows` accepts raw SQL.
`TableRows()` in `pkg/client/client.go` appends `WHERE <opts.Where>` directly.

## Build command (GOROOT is broken in this environment)
```bash
GOROOT=/opt/homebrew/Cellar/go/1.25.7_1/libexec \
GOPROXY=https://proxy.golang.org,direct \
GONOSUMDB='*' \
GOOS=linux GOARCH=amd64 \
go build -o ./bin/pgweb_linux_amd64
```
Output: `bin/pgweb_linux_amd64` (~28MB, ELF 64-bit, statically linked)
3 changes: 2 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"Bash(git checkout:*)",
"Bash(go test:*)",
"Bash(go build:*)",
"Bash(make:*)"
"Bash(make:*)",
"Bash(go env:*)"
]
}
}
2 changes: 1 addition & 1 deletion pkg/command/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

const (
// Version is the current Pgweb application version
Version = "0.17.0"
Version = "0.17.0p1"
)

var (
Expand Down
175 changes: 174 additions & 1 deletion static/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,7 @@
}

.with-pagination #output {
top: 50px !important;
top: 50px;
}

.with-pagination #pagination {
Expand Down Expand Up @@ -914,3 +914,176 @@
.ace_autocomplete .ace_active-line {
background: #eee !important;
}

/* ------------------------------------------------------------------ */
/* Advanced Search Panel */
/* ------------------------------------------------------------------ */

#pagination.adv-search-open {
height: auto;
min-height: 50px;
}

#advanced_search_panel {
clear: both;
padding: 8px 10px 4px 10px;
border-top: 1px solid #eee;
background: #fafafa;
}

.adv-search-header {
font-size: 12px;
margin-bottom: 6px;
line-height: 26px;
}

.adv-match-label {
color: #777;
margin: 0 6px;
}

.adv-conjunction-toggle {
display: inline-block;
vertical-align: middle;
}

.adv-search-row {
display: flex;
align-items: center;
margin-bottom: 4px;
gap: 6px;
}

.adv-search-row .adv-col {
width: 150px;
flex-shrink: 0;
height: 28px;
font-size: 12px;
padding: 2px 6px;
}

.adv-search-row .adv-op {
width: 110px;
flex-shrink: 0;
height: 28px;
font-size: 12px;
padding: 2px 6px;
}

.adv-search-row .adv-val {
flex: 1;
min-width: 80px;
max-width: 220px;
height: 28px;
font-size: 12px;
padding: 2px 6px;
}

.adv-search-row .adv-remove-row {
flex-shrink: 0;
padding: 2px 7px;
height: 28px;
line-height: 22px;
}

.adv-search-footer {
margin-top: 6px;
padding-bottom: 4px;
}

.adv-search-footer .btn {
margin-right: 6px;
}

#advanced-search-toggle.adv-open {
background: #f0e8ff;
border-color: #79589f;
color: #79589f;
}

#adv_search_active_badge {
font-size: 11px;
margin-right: 6px;
vertical-align: middle;
background-color: #79589f;
}

/* Per-row AND/OR connector pill */
.adv-row-conj {
display: flex;
align-items: center;
flex-shrink: 0;
width: 72px;
gap: 2px;
}

.adv-row-conj-first {
justify-content: flex-end;
}

.adv-row-conj-first span {
font-size: 11px;
color: #999;
font-weight: bold;
padding-right: 4px;
}

.adv-conj-btn {
font-size: 10px;
padding: 1px 5px;
border: 1px solid #ccc;
background: #fff;
color: #777;
border-radius: 3px;
cursor: pointer;
line-height: 16px;
}

.adv-conj-btn.active {
background: #79589f;
border-color: #79589f;
color: #fff;
}

/* Range input pair */
.adv-val-range {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
gap: 4px;
}

.adv-val-range .adv-val-from,
.adv-val-range .adv-val-to {
flex: 1;
min-width: 60px;
height: 28px;
font-size: 12px;
padding: 2px 6px;
}

.adv-range-sep {
flex-shrink: 0;
font-size: 11px;
color: #999;
}

/* Query preview box */
#adv_query_display {
margin-top: 6px;
padding: 6px 8px;
background: #f5f5f5;
border: 1px solid #e0e0e0;
border-radius: 3px;
}

#adv_query_display pre {
margin: 0 0 6px 0;
font-size: 11px;
white-space: pre-wrap;
word-break: break-all;
color: #333;
max-height: 80px;
overflow-y: auto;
}
20 changes: 20 additions & 0 deletions static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,33 @@
<input type="text" class="form-control" placeholder="Filter value" id="table_filter_value" />
<button class="btn btn-primary btn-sm apply-filters" type="submit">Apply</button>
<button class="btn btn-default btn-sm reset-filters"><i class="fa fa-times"></i></button>
<button class="btn btn-default btn-sm" id="advanced-search-toggle" type="button">Advanced <i class="fa fa-caret-down"></i></button>
</form>
<div id="advanced_search_panel" style="display:none;">
<div class="adv-search-header">
<span class="adv-match-label">Conditions:</span>
</div>
<div id="adv_search_rows">
<!-- Condition rows injected by buildAdvancedSearchRow() -->
</div>
<div class="adv-search-footer">
<button type="button" class="btn btn-default btn-xs" id="adv-add-condition"><i class="fa fa-plus"></i> Add Condition</button>
<button type="button" class="btn btn-primary btn-sm" id="adv-apply">Apply</button>
<button type="button" class="btn btn-default btn-sm" id="adv-reset"><i class="fa fa-times"></i> Clear</button>
<button type="button" class="btn btn-default btn-sm" id="adv-show-query"><i class="fa fa-code"></i> Show Query</button>
</div>
<div id="adv_query_display" style="display:none;">
<pre id="adv_query_text"></pre>
<button type="button" class="btn btn-default btn-xs" id="adv-copy-query"><i class="fa fa-copy"></i> Copy</button>
</div>
</div>
<div class="btn-group">
<button type="button" class="btn btn-default btn-sm prev-page" disabled="disabled"><i class="fa fa-angle-left"></i></button>
<button type="button" class="btn btn-default btn-sm page change-limit" title="Click to change row limit"></button>
<button type="button" class="btn btn-default btn-sm next-page"><i class="fa fa-angle-right"></i></button>
</div>
<div class="current-page" data-page="1" data-pages="1">
<span id="adv_search_active_badge" class="label label-primary" style="display:none;">Advanced Filter Active</span>
<span id="total_records"></span> rows
</div>
</div>
Expand Down
Loading