From e4b5d19091a5a05515a0f90cfd6256a9d530cd0f Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Thu, 3 Sep 2026 16:09:14 +0200 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=92=84=20Move=20Show=20and=20Sort=20t?= =?UTF-8?q?o=20the=20results=20line=20and=20align=20the=20filters=20panel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Show (page size) and Sort are result-presentation controls, not query filters — they now sit right of 'Showing X of Y result(s)' instead of competing with the query inputs for space in the filters panel, which also removes their awkward wrapping. The filters panel keeps the query inputs only (search, endpoint, time range), left-aligned; the rule that pushed the last filter to the far right is gone. --- .../src/components/audit/AuditList.vue | 19 ++++- .../src/components/audit/FiltersPanel.vue | 62 +------------- .../src/components/audit/ResultsOptions.vue | 84 +++++++++++++++++++ 3 files changed, 104 insertions(+), 61 deletions(-) create mode 100644 src/Frontend/src/components/audit/ResultsOptions.vue diff --git a/src/Frontend/src/components/audit/AuditList.vue b/src/Frontend/src/components/audit/AuditList.vue index 6ee6faba9..76deddcf9 100644 --- a/src/Frontend/src/components/audit/AuditList.vue +++ b/src/Frontend/src/components/audit/AuditList.vue @@ -4,6 +4,7 @@ import { storeToRefs } from "pinia"; import { useRoute, useRouter } from "vue-router"; import ResultsCount from "@/components/ResultsCount.vue"; import FiltersPanel from "@/components/audit/FiltersPanel.vue"; +import ResultsOptions from "@/components/audit/ResultsOptions.vue"; import AuditListItem from "@/components/audit/AuditListItem.vue"; import { computed, onBeforeMount, onBeforeUnmount, ref, watch } from "vue"; import RefreshConfig from "../RefreshConfig.vue"; @@ -182,8 +183,9 @@ watch(autoRefreshValue, (newValue) => {
-
+
+
@@ -254,6 +256,21 @@ watch(autoRefreshValue, (newValue) => { color: #fff; } +.results-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; +} + +/* ResultsCount's root uses the bootstrap .col class (flex-grow: 1), which would + push the options onto their own line; in this row both sides size to content */ +.results-row > * { + flex: 0 1 auto; + width: auto; +} + .results-table { margin-top: 1rem; margin-bottom: 5rem; diff --git a/src/Frontend/src/components/audit/FiltersPanel.vue b/src/Frontend/src/components/audit/FiltersPanel.vue index f2c7a849a..1330625a0 100644 --- a/src/Frontend/src/components/audit/FiltersPanel.vue +++ b/src/Frontend/src/components/audit/FiltersPanel.vue @@ -1,57 +1,16 @@ @@ -112,11 +59,6 @@ function findKeyByValue(searchValue: string) { min-width: 0; } -.filter:last-child { - flex-grow: 1; - place-content: flex-end; -} - .filter-label { font-weight: bold; padding-block: 0.375rem; diff --git a/src/Frontend/src/components/audit/ResultsOptions.vue b/src/Frontend/src/components/audit/ResultsOptions.vue new file mode 100644 index 000000000..08ff3042a --- /dev/null +++ b/src/Frontend/src/components/audit/ResultsOptions.vue @@ -0,0 +1,84 @@ + + + + + From f114fb71b582c2552df58c39a2110cf63258832e Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Thu, 3 Sep 2026 16:11:38 +0200 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=92=84=20Format=20result=20counts=20i?= =?UTF-8?q?n=20the=20user's=20locale?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Large audit stores easily reach nine-digit totals; 'Showing 100 of 158736340 result(s)' is unreadable. Both numbers now go through Intl.NumberFormat with the user's locale (158,736,340 / 158.736.340). ResultsCount is shared, so the heartbeats views and the throughput queue list pick this up as well. --- .../src/components/ResultsCount.spec.ts | 18 ++++++++++++++++++ src/Frontend/src/components/ResultsCount.vue | 11 +++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 src/Frontend/src/components/ResultsCount.spec.ts diff --git a/src/Frontend/src/components/ResultsCount.spec.ts b/src/Frontend/src/components/ResultsCount.spec.ts new file mode 100644 index 000000000..363162a04 --- /dev/null +++ b/src/Frontend/src/components/ResultsCount.spec.ts @@ -0,0 +1,18 @@ +import { describe, expect, test } from "vitest"; +import { render, screen } from "@testing-library/vue"; +import ResultsCount from "@/components/ResultsCount.vue"; + +describe("FEATURE: Results count", () => { + test("EXAMPLE: Large counts are formatted in the user's locale", () => { + render(ResultsCount, { props: { displayed: 100, total: 158736340 } }); + + const expected = `Showing ${(100).toLocaleString()} of ${(158736340).toLocaleString()} result(s)`; + expect(screen.getByText(expected)).toBeInTheDocument(); + }); + + test("EXAMPLE: Zero results render plainly", () => { + render(ResultsCount, { props: { displayed: 0, total: 0 } }); + + expect(screen.getByText("Showing 0 of 0 result(s)")).toBeInTheDocument(); + }); +}); diff --git a/src/Frontend/src/components/ResultsCount.vue b/src/Frontend/src/components/ResultsCount.vue index 2340aa8c7..64353a48e 100644 --- a/src/Frontend/src/components/ResultsCount.vue +++ b/src/Frontend/src/components/ResultsCount.vue @@ -1,13 +1,20 @@ From 37e39b3ad00a88474a41bfffbc06e6f8ca451269 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Thu, 3 Sep 2026 17:28:59 +0200 Subject: [PATCH 3/5] =?UTF-8?q?=E2=9C=A8=20Refresh=20lives=20in=20the=20qu?= =?UTF-8?q?ery=20bar=20and=20cancels=20the=20running=20query?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The refresh cluster occupied its own row above the filters; it now sits right-aligned inside the query bar so the whole query surface is one row. While a query is in flight the button flips from Refresh to an enabled Cancel that aborts the running query through the store; the abort propagates through ServiceControl and terminates the database-side query. Once results land, the results line reports what they cost: "Showing 100 of 158,736,340 result(s) · took 2.7 s". The timing is forgotten with the results when the view is left. ActionButton hard-coupled loading to disabled, which would have left the button dead exactly when a user most wants an escape hatch; it gains disableOnLoading (default true, existing uses unchanged) and the refresh/cancel button opts out. The spec stubs ActionButton with the real disabled semantics so that coupling can't silently return. --- src/Frontend/src/components/ActionButton.vue | 8 ++- .../src/components/RefreshConfig.spec.ts | 57 ++++++++++++++++--- src/Frontend/src/components/RefreshConfig.vue | 43 +++++++++++--- .../src/components/ResultsCount.spec.ts | 13 +++++ src/Frontend/src/components/ResultsCount.vue | 10 +++- .../src/components/audit/AuditList.spec.ts | 20 ++++++- .../src/components/audit/AuditList.vue | 13 +++-- .../src/components/audit/FiltersPanel.vue | 10 ++++ src/Frontend/src/stores/AuditStore.spec.ts | 28 +++++++++ src/Frontend/src/stores/AuditStore.ts | 13 +++++ 10 files changed, 190 insertions(+), 25 deletions(-) diff --git a/src/Frontend/src/components/ActionButton.vue b/src/Frontend/src/components/ActionButton.vue index 1b94fe11a..11f48a338 100644 --- a/src/Frontend/src/components/ActionButton.vue +++ b/src/Frontend/src/components/ActionButton.vue @@ -12,6 +12,9 @@ interface Props { iconPosition?: "left" | "right"; disabled?: boolean; loading?: boolean; + /** Loading normally disables the button; opt out for buttons that stay + * actionable while work runs (e.g. a cancel button). */ + disableOnLoading?: boolean; tooltip?: string; ariaLabel?: string; type?: "button" | "submit" | "reset"; @@ -23,6 +26,7 @@ const props = withDefaults(defineProps(), { iconPosition: "left", disabled: false, loading: false, + disableOnLoading: true, type: "button", }); @@ -44,8 +48,8 @@ const sizeClasses = {