Skip to content
Draft
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## unreleased

- Added loading indicators and progress bars for pages that stream results while queries run.

- Now a tab with a `color` is underlined only when it is active. ([#791](https://github.com/sqlpage/SQLPage/issues/791))

- Shell submenus stay open again when you click inside them. ([#1011](https://github.com/sqlpage/SQLPage/issues/1011))
Expand Down
3 changes: 3 additions & 0 deletions examples/official-site/examples/show-progress/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# SQLPage display progress

This is a very simple example of a page that shows a spinner to indicate page is loading.
19 changes: 19 additions & 0 deletions examples/official-site/examples/show-progress/go.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-- Disable actix-web compression middleware:
-- https://github.com/actix/actix-web/issues/3410
SELECT 'http_header' AS component, 'Identity' AS "Content-Encoding";

Comment on lines +1 to +4

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this still? actix/actix-web#341 is closed

@djyotta djyotta Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not immediately obvious to me how actix/actix-web#341 is related.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ope, sorry missed a 0 at the end

actix/actix-web#3410

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I implemented this not as a work around for some buffering issue, but because I have some legitimately long running queries so the spinner helps indicate something is happening.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, but its fixed upstream now. I think the better approach would be to update the actix-web dependency and use the upstream fix :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I misunderstood your comment at the first. My bad.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nw, thanks for the PR, excited to see the next revision :)
please re-request review when ready

SELECT 'shell' AS component, 'light' AS theme;

-- can disable the spinner to show only progress bar
SELECT 'loader-start' AS component, '' AS spinner;

SELECT 'progress' AS component,
NULL AS percent,
'sm' AS size,
'yellow' AS color,
'Working on it' AS stage;
SELECT sqlpage.fetch('https://example.com');

SELECT 'loader-stop' AS component;

SELECT 'text' AS component, 'Processing complete.' AS contents;
57 changes: 57 additions & 0 deletions examples/official-site/examples/show-progress/index.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
-- Disable actix-web compression middleware:
-- https://github.com/actix/actix-web/issues/3410
SELECT 'http_header' AS component, 'Identity' AS "Content-Encoding";

SELECT 'shell' AS component, 'dark' AS theme;

SELECT 'loader-start' AS component,
-- pick from the tabler spinners: https://tabler.io/docs/components/spinners
"spinner-border" spinner,
"sm" AS size,
"red" AS color;

SELECT 'progress' AS component,
'sm' AS size,
'yellow' AS color,
'0' AS percent,
'Fething data' AS stage;
SELECT sqlpage.fetch('https://example.com');

/* percent property is optional */
SELECT 'progress' AS component,
NULL AS percent,
'sm' AS size,
'yellow' AS color,
'Doing something' AS stage;
SELECT sqlpage.fetch('https://example.com');

/* stage property is optional */
SELECT 'progress' AS component,
'sm' AS size,
'yellow' AS color,
'40' AS percent;
SELECT sqlpage.fetch('https://example.com');

/* multiple rows */
SELECT 'progress' AS component, 'sm' AS size, 'yellow' AS color;
SELECT '70' AS percent, 'Fetching data' AS stage
SELECT sqlpage.fetch('https://example.com');
SELECT '80' AS percent, 'Fetching more data' AS stage;
SELECT sqlpage.fetch('https://example.com');
SELECT '90' AS percent, 'Fetching again' AS stage;
SELECT sqlpage.fetch('https://example.com');

SELECT '100' AS percent;

SELECT 'loader-stop' AS component;

SELECT 'text' AS component,
'It works!' AS title,
TRUE AS center,
'Page is loaded.' AS contents;

SELECT 'button' AS component;
SELECT 'Go' AS title, './go.sql' AS link;

-- can use progress on it's own
SELECT 'progress' AS component, 'sm' AS size, 'Waiting for user' AS stage;
65 changes: 65 additions & 0 deletions examples/official-site/sqlpage/migrations/01_documentation.sql
Original file line number Diff line number Diff line change
Expand Up @@ -1464,6 +1464,70 @@ GROUP BY name;
```
', NULL);

INSERT INTO component(name, icon, description, introduced_in_version) VALUES
('loader-start', 'refresh', 'Display a spinner to indicate page is loading.', '0.47.0');

INSERT INTO parameter(component, name, description_md, type, top_level, optional) SELECT 'loader-start', * FROM (VALUES
('id', 'HTML identifier for the loading container.', 'TEXT', TRUE, TRUE),
('spinner', '
The name of a [spinner](https://tabler.io/docs/components/spinners) (from tabler.io).
Default is "spinner-border".
Set to the empty string to disable the spinner - e.g. to display only progress
updates.
', 'TEXT', TRUE, TRUE),
('size', 'Size of the spinner (e.g. sm, lg) [see here](https://tabler.io/docs/components/progress)', 'TEXT', TRUE, TRUE),
('color', 'Color of the spinner', 'COLOR', TRUE, TRUE)
) x;

INSERT INTO component(name, icon, description, introduced_in_version) VALUES
('loader-stop', 'refresh-off', '
Hide the spinner displayed by the loader-start component.
', '0.47.0');

INSERT INTO component(name, icon, description, introduced_in_version) VALUES
('progress', 'time-duration-15', 'Display a progress bar.', '0.47.0');

INSERT INTO parameter(component, name, description_md, type, top_level, optional) SELECT 'progress', * FROM (VALUES
-- top-level
('percent', 'Number between 0 and 100.', 'INTEGER', TRUE, TRUE),
('stage', 'A message to display under the progress bar to indicate which stage the process is at.', 'TEXT', TRUE, TRUE),
('color', 'The fill color of the progress bar', 'COLOR', TRUE, TRUE),
('size', 'The size of the progress bar (e.g. sm, lg) [see here](https://tabler.io/docs/components/progress)', 'TEXT', TRUE, TRUE),
-- item-level
('percent', 'Number between 0 and 100.', 'INTEGER', FALSE, TRUE),
('stage', 'A message to display under the progress bar to indicate which stage the process is at.', 'TEXT', FALSE, TRUE)
) x;

INSERT INTO example(component, description, properties) VALUES
('loader-start', '
Will be displayed until [loader-stop](/documentation.sql?component=loader-stop) is selected.

See the [live example](/examples/show-progress/).

Source is [here](https://github.com/lovasoa/SQLpage/tree/main/examples/official-site/examples/show-progress)
', NULL),
('loader-stop', '
Hide the spinner displayed by the [loader-start](/documentation.sql?component=loader-start) component.

See the [live example](/examples/show-progress/).

Source is [here](https://github.com/lovasoa/SQLpage/tree/main/examples/official-site/examples/show-progress)
', NULL),
('progress', '
Can be used on it''s own (will persist on the page) or with
[loader-start](/documentation.sql?component=loader-start) and
[loader-stop](/documentation.sql?component=loader-stop) to hide the progress bar once processing
is complete.

Subsequent uses of this component and/or any rows will
update the progress bar.

See the [live example](/examples/show-progress/).

Source is [here](https://github.com/lovasoa/SQLpage/tree/main/examples/official-site/examples/show-progress)
', NULL)
;

INSERT INTO component(name, icon, description) VALUES
('shell', 'layout-navbar', '
Customize the overall layout, header and footer of the page.
Expand Down Expand Up @@ -1549,6 +1613,7 @@ You see the [page layouts demo](./examples/layouts.sql) for a live example of th
{"link": "/examples/multistep-form", "title": "Forms", "icon": "edit"},
{"link": "/examples/handle_picture_upload.sql", "title": "File uploads", "icon": "upload"},
{"link": "/examples/authentication/", "title": "Password protection", "icon": "password-user"},
{"link": "/examples/show-progress/", "title": "Tracking progress of background tasks", "icon": "loader-2"},
{"link": "//github.com/sqlpage/SQLPage/blob/main/examples/", "title": "All examples & demos", "icon": "code"}
]},
{"title": "Community", "submenu": [
Expand Down
46 changes: 46 additions & 0 deletions frontend/src/sqlpage.css
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,49 @@ See https://github.com/tabler/tabler/issues/2404
color: inherit;
text-decoration: underline;
}

/* loader container */
.sqlpage-loader-container {
position: fixed;
text-align: center;
left: 50vw;
top: 50vh;
margin-top: -5.5em;
margin-left: -87.5px;
padding-bottom: 2em;
height: 9em;
width: 175px;
}
.sqlpage-loader-container:has(.status) {
position: inherit;
text-align: inherit;
left: inherit;
top: inherit;
margin-top: inherit;
margin-left: inherit;
padding-bottom: inherit;
height: inherit;
width: inherit;
}
div.sqlpage-loader-start:has(+ .sqlpage-loader-stop) {
/* hide if followed by sqlpage-loader-stop */
display: none;
}
/* end loader container */

/* progress container */
.sqlpage-progress-container {
margin: 1em 0 1em;
}
div.sqlpage-progress-container:has(+ .sqlpage-progress-container) {
/* hide if followed by sqlpage-progress-container */
display: none;
}
.sqlpage-progress-container label {
text-align: left;
color: var(--tblr-text-primary);
}
.sqlpage-progress-container label:after {
content: "…";
}
/* end progress container */
6 changes: 6 additions & 0 deletions sqlpage/templates/loader-start.handlebars
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<div {{#if id}}id="{{id}}"{{/if}} class="sqlpage-loader-start">
<div class="sqlpage-loader-container">
<span class="{{default spinner "spinner-border"}}
{{#if size}}spinner-border-{{size}}{{/if}}
{{#if color}}text-{{color}}{{/if}}">
</span>
Comment on lines +1 to +6

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

errors between loader-start and loader-stop show up in the spinner's box

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I knew about this one. I didn't even begin to think about how to solve that... lol, just make sure it doesn't error.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can fix it by closing the <div>. something like

<div {{#if id}}id="{{id}}"{{/if}} class="sqlpage-loader" role="status">
  <span class="{{default spinner "spinner-border"}} {{#if color}}text-{{color}}{{/if}}"></span>
</div>

@djyotta djyotta Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could be forgetting, but I thought the whole hack here was leaving the div open so the spinner disappears when it's closed... My CSS evolved over time though, and it looks as though closing the div should work.

I just recall leaving it open was important for some reason.

It's closed in the sqlpage-loader-stop element.

3 changes: 3 additions & 0 deletions sqlpage/templates/loader-stop.handlebars
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
</div>
</div>
<div class="sqlpage-loader-stop"></div>
42 changes: 42 additions & 0 deletions sqlpage/templates/progress.handlebars
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<div class="sqlpage-progress-container">
<div class="progress {{~#if size}} progress-{{size}}{{/if}}">
<div
id="sqlpage-loading-{{default stage "progress"}}"
class="progress-bar
{{~#if (not percent)}} progress-bar-indeterminate{{/if}}
{{~#if color}} bg-{{color}}{{/if}}"
role="progressbar"
aria-valuenow="{{percent}}"
{{~#if percent}}style="width: {{percent}}%; display: block"{{/if}}
Comment on lines +6 to +10

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SELECT 'progress' AS component, 0 AS percent renders progress-bar-indeterminate because of an improper null check

aria-valuemin="0" aria-valuemax="100"
{{~#if stage}}aria-label="{{stage}}"{{/if}}>
</div>
</div>
{{~#if stage}}
<br>
<label for="sqlpage-loading-{{default stage "progress"}}">{{stage}}</label>
{{/if}}
Comment on lines +15 to +18

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

</div>
{{#each_row}}
{{#if (or percent stage)}}
<div class="sqlpage-progress-container">
<div class="progress {{~#if ../size}} progress-{{../size}}{{/if}}">
<div
id="sqlpage-loading-{{default stage "progress"}}"
class="progress-bar
{{~#if (not percent)}} progress-bar-indeterminate{{/if}}
{{~#if ../color}} bg-{{../color}}{{/if}}"
role="progressbar"
aria-valuenow="{{percent}}"
{{~#if percent}}style="width: {{percent}}%; display: block"{{/if}}
aria-valuemin="0" aria-valuemax="100"
{{~#if stage}}aria-label="{{stage}}"{{/if}}>
</div>
</div>
{{~#if stage}}
<br>
<label for="sqlpage-loading-{{default stage "progress"}}">{{stage}}</label>
{{/if}}
</div>
{{/if}}
{{/each_row}}
9 changes: 9 additions & 0 deletions tests/end-to-end/fixtures/progress/index.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
SELECT 'shell' AS component, 'Progress test' AS title;

SELECT 'loader-start' AS component, 'completed-loader' AS id;
SELECT 'loader-stop' AS component;

SELECT 'progress' AS component, 25 AS percent, 'blue' AS color;
SELECT 100 AS percent;

SELECT 'loader-start' AS component, 'active-loader' AS id, 'red' AS color;
15 changes: 15 additions & 0 deletions tests/end-to-end/fixtures/progress/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { expect, test } from "../../fixture";

Check failure on line 1 in tests/end-to-end/fixtures/progress/test.ts

View workflow job for this annotation

GitHub Actions / compile_and_lint

Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean '../../fixture.js'?

test("stops completed loaders and shows only the latest progress", async ({
page,

Check failure on line 4 in tests/end-to-end/fixtures/progress/test.ts

View workflow job for this annotation

GitHub Actions / compile_and_lint

Binding element 'page' implicitly has an 'any' type.
}) => {
await expect(page.locator("#completed-loader")).toBeHidden();
await expect(page.locator("#active-loader .spinner-border")).toBeVisible();
await expect(
page.locator(".sqlpage-progress-container").first(),
).toBeHidden();
const progress = page.getByRole("progressbar");
await expect(progress).toHaveCount(1);
await expect(progress).toHaveAttribute("aria-valuenow", "100");
await expect(progress).toHaveClass(/bg-blue/);
});
Loading