LCOV - code coverage report
Current view: top level - dashql-app/src/session/persistence - query_result_cache_controls.tsx Coverage Total Hit
Test: DashQL 915828b Lines: 100.0 % 50 50
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as React from 'react';
       2              : import * as styles from './query_result_cache_controls.module.css';
       3              : 
       4              : import { SyncIcon } from '../../ui/foundations/symbol_icon.js';
       5              : 
       6              : import { QueryExecutionState, QueryExecutionStatus } from '../../query/query_execution_state.js';
       7              : import { useRelativeTime } from '../../utils/time_format.js';
       8              : import { ButtonVariant, IconButton } from '../../ui/foundations/button.js';
       9              : import { Tag } from '../../ui/foundations/tag.js';
      10              : import { AnchoredOverlay } from '../../ui/foundations/anchored_overlay.js';
      11              : import { AnchorAlignment, AnchorSide } from '../../ui/foundations/anchored_position.js';
      12              : 
      13              : interface QueryResultCacheControlsProps {
      14              :     query: QueryExecutionState | null;
      15              : }
      16              : 
      17              : interface CachedResultBeanProps {
      18              :     /// The time the cached result was written
      19              :     cachedAtMs: number;
      20              : }
      21              : 
      22              : /// The "Cached" tag of a result card. Clicking it tells since when the result is cached.
      23            1 : export const CachedResultBean: React.FC<CachedResultBeanProps> = ({ cachedAtMs }) => {
      24            1 :     const [open, setOpen] = React.useState(false);
      25            1 :     const relativeTime = useRelativeTime(cachedAtMs);
      26            1 :     return (
      27            1 :         <AnchoredOverlay
      28            1 :             open={open}
      29            1 :             onOpen={() => setOpen(true)}
      30            1 :             onClose={() => setOpen(false)}
      31            1 :             side={AnchorSide.OutsideBottom}
      32            1 :             align={AnchorAlignment.End}
      33            1 :             anchorOffset={4}
      34            1 :             renderAnchor={(p: object) => (
      35            1 :                 <button
      36            1 :                     {...(p as React.ButtonHTMLAttributes<HTMLButtonElement>)}
      37            1 :                     type="button"
      38            1 :                     className={styles.cached_result_button}
      39            1 :                     aria-label="Show since when the result is cached"
      40            1 :                     data-testid="cached-result"
      41              :                 >
      42            1 :                     <Tag className={styles.cached_result_bean}>Cached</Tag>
      43            1 :                 </button>
      44              :             )}
      45              :         >
      46            1 :             <div className={styles.cached_result_detail} role="dialog" aria-label="Cached result" data-testid="cached-result-detail">
      47            1 :                 <div className={styles.cached_result_detail_title}>
      48            1 :                     Cached {relativeTime != null ? lowerFirst(relativeTime) : null}
      49            1 :                 </div>
      50            1 :                 <div className={styles.cached_result_detail_time}>
      51            1 :                     {new Date(cachedAtMs).toLocaleString()}
      52            1 :                 </div>
      53            1 :             </div>
      54            1 :         </AnchoredOverlay>
      55              :     );
      56            1 : };
      57              : 
      58              : /// Lowercase the first letter so "Just now"/"Yesterday" read naturally after a verb
      59            1 : function lowerFirst(text: string): string {
      60            1 :     return text.charAt(0).toLowerCase() + text.slice(1);
      61            1 : }
      62              : 
      63              : /// Whether the query has a result to describe (succeeded, not since cache-deleted).
      64            1 : function hasResult(query: QueryExecutionState | null): boolean {
      65            1 :     return query?.status === QueryExecutionStatus.SUCCEEDED && query?.cacheDeleted !== true;
      66            1 : }
      67              : 
      68              : /// The timestamp to show as "Executed <relative>": for a cache hit this is the cached entry's write
      69              : /// time (the original execution); otherwise it's when this run succeeded (which is "now" right after
      70              : /// a cache miss). Falls back to null when neither is available yet.
      71              : function executedAtMs(query: QueryExecutionState | null): number | null {
      72              :     if (query == null) {
      73              :         return null;
      74              :     }
      75              :     if (query.servedFromCache && query.cachedAt != null) {
      76              :         return query.cachedAt.getTime();
      77              :     }
      78              :     return query.metrics?.querySucceededAt?.getTime() ?? null;
      79              : }
      80              : 
      81              : /// The execution age label shown in the entry status bar: "Executed <relative time>" (e.g.
      82              : /// "Executed 8 minutes ago", "Executed just now"). Shown for any succeeded result regardless of
      83              : /// cache hit/miss; a cache hit reports the original execution time, a fresh run reports "now". The
      84              : /// relative time refreshes on an interval.
      85              : export const QueryResultCacheLabel: React.FC<QueryResultCacheControlsProps> = ({ query }) => {
      86              :     const relativeTime = useRelativeTime(executedAtMs(query));
      87              :     if (!hasResult(query) || relativeTime == null) {
      88              :         return null;
      89              :     }
      90              :     return (
      91              :         <span className={styles.cache_label}>
      92              :             Executed {lowerFirst(relativeTime)}
      93              :         </span>
      94              :     );
      95              : };
      96              : 
      97              : interface QueryResultRerunButtonProps {
      98              :     query: QueryExecutionState | null;
      99              :     /// Run the script of this result again without reading cached results
     100              :     onRerun?: () => void;
     101              : }
     102              : 
     103              : /// The Refresh icon shown in the entry status bar. Shown for any succeeded result; clicking it runs the script and
     104              : /// the scripts it references again against their backends, without reading cached results.
     105            1 : export const QueryResultRerunButton: React.FC<QueryResultRerunButtonProps> = ({ query, onRerun }) => {
     106            1 :     if (!hasResult(query)) {
     107            1 :         return null;
     108              :     }
     109            1 :     return (
     110            1 :         <IconButton
     111            1 :             className={styles.rerun_button}
     112            1 :             variant={ButtonVariant.Invisible}
     113            1 :             onClick={onRerun}
     114            1 :             disabled={onRerun == null}
     115            1 :             aria-label="Refresh"
     116            1 :             description="Refresh"
     117              :         >
     118            1 :             <SyncIcon size={16} />
     119            1 :         </IconButton>
     120              :     );
     121            1 : };
        

Generated by: LCOV version 2.0-1