summaryrefslogtreecommitdiffhomepage
path: root/src/features/concurrency/ui/AutoReduceBanner.svelte
blob: 132ebc7809b0d2e273afbd370218bb306579c669 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<script lang="ts">
  import type { AutoReduceNotice } from "../logic/view-model";
  import type { RestoreOutcome } from "../logic/types";

  let {
    notice,
    onRestore,
    onDismiss,
  }: {
    /** The auto-reduce banner view (providerId + message + from/current limit). */
    notice: AutoReduceNotice;
    /**
     * "Restore to N" — PUT the limit back to `fromLimit`. Returns the outcome so
     * a FAILED restore surfaces an inline error here (the banner owns its error
     * display; the parent only refreshes on success).
     */
    onRestore: (providerId: string, limit: number) => Promise<RestoreOutcome>;
    /** Hide this banner locally (persists hidden while autoReduced stays true). */
    onDismiss: (providerId: string) => void;
  } = $props();

  let restoring = $state(false);
  /** Inline restore error (e.g. "Concurrency service not available"); cleared on retry. */
  let error = $state<string | null>(null);

  async function handleRestore(): Promise<void> {
    restoring = true;
    error = null;
    // The parent PUTs the limit + refreshes status on success; the banner clears
    // once the next poll shows autoReduced===false. On failure the outcome is
    // bubbled back here so the error shows inline next to the button.
    const result = await onRestore(notice.providerId, notice.fromLimit);
    restoring = false;
    if (!result.ok) {
      error = result.error;
    }
  }
</script>

<div
  class="alert alert-warning flex flex-col gap-2 py-2 text-xs"
  role="status"
  data-testid={`auto-reduce-banner-${notice.providerId}`}
>
  <div class="flex items-start gap-2">
    <span class="shrink-0">⚠</span>
    <div class="flex-1">
      <p>{notice.message}</p>
      <p class="opacity-70">
        Was {notice.fromLimit}, now {notice.currentLimit}.
      </p>
    </div>
    <div class="flex shrink-0 items-center gap-1">
      <!-- The "Restore to N" text stays visible while loading (only the spinner is
           prepended) so the button keeps its accessible name during the PUT — a
           spinner-only button loses its name for screen-reader users. -->
      <button
        type="button"
        class="btn btn-warning btn-xs gap-1"
        disabled={restoring}
        onclick={handleRestore}
      >
        {#if restoring}
          <span class="loading loading-spinner loading-xs"></span>
        {/if}
        Restore to {notice.fromLimit}
      </button>
      <button
        type="button"
        class="btn btn-ghost btn-xs"
        aria-label={`Dismiss auto-reduce notice for ${notice.providerId}`}
        onclick={() => onDismiss(notice.providerId)}
      >
        ✕
      </button>
    </div>
  </div>
  {#if error}
    <p class="font-mono text-error" data-testid={`restore-error-${notice.providerId}`}>{error}</p>
  {/if}
</div>