Skip to content

Commit 986f9d4

Browse files
Merge pull request #1817 from rocket-admin/auto-configure-progress-loader
feat(auto-configure): live progress loader with streaming + summary
2 parents a465a72 + f2c9982 commit 986f9d4

4 files changed

Lines changed: 519 additions & 36 deletions

File tree

frontend/src/app/components/auto-configure/auto-configure.component.css

Lines changed: 192 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@
4646
display: flex;
4747
flex-direction: column;
4848
align-items: center;
49-
padding: 48px 32px 36px;
50-
gap: 16px;
49+
padding: 40px 32px 24px;
50+
gap: 10px;
5151
}
5252

5353
.card__title {
@@ -64,6 +64,16 @@
6464
}
6565
}
6666

67+
.card__head-detail {
68+
width: 100%;
69+
min-height: 64px;
70+
display: flex;
71+
flex-direction: column;
72+
align-items: center;
73+
justify-content: center;
74+
gap: 8px;
75+
}
76+
6777
.card__subtitle {
6878
margin: 0;
6979
font-size: 14px;
@@ -160,7 +170,6 @@
160170
position: relative;
161171
width: 72px;
162172
height: 72px;
163-
margin-bottom: 8px;
164173
}
165174

166175
.spinner__ring {
@@ -191,33 +200,195 @@
191200
to { transform: rotate(-360deg); }
192201
}
193202

194-
/* ── Indeterminate progress bar ── */
203+
/* ── Success checkmark (replaces spinner on complete) ── */
204+
205+
.success-mark {
206+
width: 72px;
207+
height: 72px;
208+
}
195209

196-
.progress-bar {
210+
.success-mark svg {
197211
width: 100%;
198-
max-width: 320px;
199-
height: 4px;
200-
border-radius: 2px;
201-
background: var(--color-primaryPalette-200);
202-
overflow: hidden;
212+
height: 100%;
213+
overflow: visible;
214+
}
215+
216+
.success-mark__circle {
217+
fill: none;
218+
stroke: var(--color-successPalette-500);
219+
stroke-width: 2;
220+
stroke-linecap: round;
221+
stroke-dasharray: 158;
222+
stroke-dashoffset: 158;
223+
animation: success-circle 500ms ease-out forwards;
224+
transform-origin: 50% 50%;
225+
}
226+
227+
.success-mark__check {
228+
fill: none;
229+
stroke: var(--color-successPalette-500);
230+
stroke-width: 3.5;
231+
stroke-linecap: round;
232+
stroke-linejoin: round;
233+
stroke-dasharray: 36;
234+
stroke-dashoffset: 36;
235+
animation: success-check 350ms 400ms ease-out forwards;
236+
}
237+
238+
@keyframes success-circle {
239+
to { stroke-dashoffset: 0; }
240+
}
241+
242+
@keyframes success-check {
243+
to { stroke-dashoffset: 0; }
244+
}
245+
246+
/* ── Summary chips (shown on complete) ── */
247+
248+
.summary-chips {
249+
display: flex;
250+
flex-wrap: wrap;
251+
justify-content: center;
252+
gap: 8px;
203253
margin-top: 4px;
204254
}
205255

256+
.summary-chip {
257+
display: inline-flex;
258+
align-items: baseline;
259+
gap: 4px;
260+
padding: 4px 10px;
261+
border-radius: 999px;
262+
background: var(--color-successPalette-50);
263+
color: var(--color-successPalette-700);
264+
font-size: 12px;
265+
font-weight: 500;
266+
line-height: 1.4;
267+
white-space: nowrap;
268+
animation: chip-enter 280ms ease-out;
269+
}
270+
271+
.summary-chip strong {
272+
font-size: 13px;
273+
font-weight: 700;
274+
}
275+
206276
@media (prefers-color-scheme: dark) {
207-
.progress-bar {
208-
background: var(--color-primaryPalette-800);
277+
.summary-chip {
278+
background: var(--color-successPalette-900);
279+
color: var(--color-successPalette-100);
209280
}
210281
}
211282

212-
.progress-bar__fill {
213-
width: 40%;
214-
height: 100%;
215-
border-radius: 2px;
216-
background: var(--color-accentedPalette-500);
217-
animation: indeterminate 1.5s ease-in-out infinite;
283+
@keyframes chip-enter {
284+
from { transform: scale(0.85); opacity: 0; }
285+
to { transform: scale(1); opacity: 1; }
218286
}
219287

220-
@keyframes indeterminate {
221-
0% { transform: translateX(-100%); }
222-
100% { transform: translateX(350%); }
288+
/* ── Live status log ── */
289+
290+
.status-log {
291+
list-style: none;
292+
margin: 0;
293+
padding: 0;
294+
width: 100%;
295+
max-width: 360px;
296+
height: 78px;
297+
display: flex;
298+
flex-direction: column;
299+
justify-content: flex-end;
300+
gap: 8px;
301+
overflow: hidden;
302+
mask-image: linear-gradient(to bottom, transparent 0, black 28px);
303+
-webkit-mask-image: linear-gradient(to bottom, transparent 0, black 28px);
304+
}
305+
306+
.status-log__item {
307+
display: flex;
308+
align-items: center;
309+
gap: 10px;
310+
font-size: 13px;
311+
color: var(--color-primaryPalette-400);
312+
text-align: left;
313+
line-height: 1.35;
314+
animation: status-log-enter 240ms ease-out;
315+
}
316+
317+
.status-log__item_done {
318+
color: var(--color-primaryPalette-200);
319+
font-size: 12px;
320+
}
321+
322+
.status-log__item_current {
323+
color: var(--color-primaryPalette-800);
324+
font-weight: 500;
325+
}
326+
327+
@media (prefers-color-scheme: dark) {
328+
.status-log__item_done {
329+
color: var(--color-primaryPalette-500);
330+
}
331+
.status-log__item_current {
332+
color: var(--color-primaryPalette-50);
333+
}
334+
}
335+
336+
.status-log__icon {
337+
flex-shrink: 0;
338+
width: 16px;
339+
height: 16px;
340+
display: inline-flex;
341+
align-items: center;
342+
justify-content: center;
343+
}
344+
345+
.status-log__check {
346+
color: var(--color-primaryPalette-200);
347+
font-size: 13px;
348+
font-weight: 700;
349+
line-height: 1;
350+
}
351+
352+
@media (prefers-color-scheme: dark) {
353+
.status-log__check {
354+
color: var(--color-primaryPalette-500);
355+
}
356+
}
357+
358+
.status-log__spinner {
359+
width: 12px;
360+
height: 12px;
361+
border-radius: 50%;
362+
border: 2px solid var(--color-accentedPalette-200);
363+
border-top-color: var(--color-accentedPalette-500);
364+
animation: tiny-spin 0.8s linear infinite;
365+
}
366+
367+
@keyframes tiny-spin {
368+
to { transform: rotate(360deg); }
369+
}
370+
371+
@keyframes status-log-enter {
372+
from { transform: translateY(-6px); opacity: 0; }
373+
to { transform: translateY(0); opacity: 1; }
374+
}
375+
376+
.card__status {
377+
margin: 0;
378+
min-height: 20px;
379+
font-size: 13px;
380+
color: var(--color-primaryPalette-500);
381+
text-align: center;
382+
line-height: 1.4;
383+
max-width: 360px;
384+
}
385+
386+
.card__status_error {
387+
color: var(--color-warnPalette-500);
388+
}
389+
390+
@media (prefers-color-scheme: dark) {
391+
.card__status {
392+
color: var(--color-primaryPalette-200);
393+
}
223394
}

frontend/src/app/components/auto-configure/auto-configure.component.html

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,42 @@
11
<div class="page">
22
<div class="card">
33
<div class="card__body">
4-
<div class="spinner">
4+
<div *ngIf="!done()" class="spinner">
55
<div class="spinner__ring spinner__ring--outer"></div>
66
<div class="spinner__ring spinner__ring--inner"></div>
77
</div>
8-
<h2 class="card__title">Configuring your database</h2>
9-
<p class="card__subtitle">We're analyzing your structure and applying the best settings. This is running in the background.</p>
10-
<div class="progress-bar">
11-
<div class="progress-bar__fill"></div>
8+
<div *ngIf="done()" class="success-mark" aria-label="Configuration complete">
9+
<svg viewBox="0 0 52 52" xmlns="http://www.w3.org/2000/svg">
10+
<circle class="success-mark__circle" cx="26" cy="26" r="25"></circle>
11+
<path class="success-mark__check" d="M14 27 l8 8 l16 -18"></path>
12+
</svg>
1213
</div>
14+
<h2 class="card__title">{{ done() ? 'Your database is ready' : 'Configuring your database' }}</h2>
15+
<div class="card__head-detail">
16+
<p *ngIf="!done()" class="card__subtitle">We're analyzing your structure and applying the best settings. This is running in the background.</p>
17+
<div *ngIf="hasSummary()" class="summary-chips">
18+
<span class="summary-chip"><strong>{{ tablesConfigured().size }}</strong> {{ tablesConfigured().size === 1 ? 'table' : 'tables' }}</span>
19+
<span *ngIf="widgetsCount() > 0" class="summary-chip"><strong>{{ widgetsCount() }}</strong> {{ widgetsCount() === 1 ? 'widget' : 'widgets' }}</span>
20+
<span *ngIf="settingsCount() > 0" class="summary-chip"><strong>{{ settingsCount() }}</strong> {{ settingsCount() === 1 ? 'setting' : 'settings' }}</span>
21+
</div>
22+
<p *ngIf="done() && !hasSummary()" class="card__subtitle">All set — opening your dashboard…</p>
23+
</div>
24+
25+
<ul class="status-log" *ngIf="!errorText(); else errorBlock">
26+
<li *ngFor="let m of messages(); let last = last; trackBy: trackByIndex"
27+
class="status-log__item"
28+
[class.status-log__item_current]="last"
29+
[class.status-log__item_done]="!last">
30+
<span class="status-log__icon" aria-hidden="true">
31+
<span *ngIf="!last" class="status-log__check">&#10003;</span>
32+
<span *ngIf="last" class="status-log__spinner"></span>
33+
</span>
34+
<span class="status-log__text">{{ m }}</span>
35+
</li>
36+
</ul>
37+
<ng-template #errorBlock>
38+
<p class="card__status card__status_error">{{ errorText() }}</p>
39+
</ng-template>
1340
</div>
1441
<div class="card__footer">
1542
<p class="card__footer-text">You can start working — setup will continue in the background.</p>

0 commit comments

Comments
 (0)