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 : };
|