@agent-native/core-corpus 0.209.2 → 0.209.3-nightly-20261016212700
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/analytics/actions/update-dashboard.ts +9 -0
- package/corpus/templates/analytics/app/components/auth/SqlQuerySnapshotSessionBridge.tsx +25 -0
- package/corpus/templates/analytics/app/components/dashboard/PanelFreshnessClock.tsx +140 -0
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +104 -48
- package/corpus/templates/analytics/app/components/dashboard/panel-freshness.ts +171 -0
- package/corpus/templates/analytics/app/global.css +59 -0
- package/corpus/templates/analytics/app/i18n/en-US.ts +8 -0
- package/corpus/templates/analytics/app/lib/cost-tracker.ts +122 -16
- package/corpus/templates/analytics/app/lib/query-metrics.ts +50 -13
- package/corpus/templates/analytics/app/lib/sql-query-cache.ts +697 -0
- package/corpus/templates/analytics/app/lib/sql-query-freshness.ts +85 -0
- package/corpus/templates/analytics/app/lib/sql-query-snapshot-session.ts +145 -0
- package/corpus/templates/analytics/app/lib/sql-query.ts +366 -36
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +73 -18
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/filter-vars.ts +45 -12
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +5 -0
- package/corpus/templates/analytics/app/root.tsx +2 -0
- package/corpus/templates/analytics/changelog/2026-10-14-dashboard-charts-now-reappear-instantly-after-a-reload-from-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-14-dashboard-panels-show-fetch-time-in-a-clock-and-label-only-st.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-15-dashboard-query-costs-and-refresh-confirmations-reflect-actu.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-15-dashboard-results-disappear-when-access-is-withdrawn-and-exp.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-16-dashboard-freshness-stays-in-the-title-row-with-compact-upda.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-16-dashboard-update-status-remains-visible-for-sources-without-.md +6 -0
- package/corpus/templates/analytics/server/lib/bigquery.ts +35 -4
- package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +2 -3
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +9 -6
- package/corpus/templates/analytics/server/lib/first-party-analytics-cache.ts +2 -2
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +4 -2
- package/corpus/templates/analytics/shared/sql-query-contract.ts +49 -0
- package/corpus/templates/assets/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/brain/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/calendar/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/chat/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/clips/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/clips/actions/list-pending-recording-context.ts +57 -21
- package/corpus/templates/clips/actions/request-recording-context.ts +12 -1
- package/corpus/templates/clips/app/i18n/en-US.ts +4 -2
- package/corpus/templates/clips/changelog/2026-10-15-clips-keeps-rewind-cleanup-warnings-visible.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-15-clips-keeps-saving-earlier-screen-time-when-local-storage-is.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-15-clips-retries-temporary-save-failures-releases-timed-out-ear.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-15-earlier-screen-time-now-works-with-window-and-camera-recordi.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-15-earlier-screen-time-requests-are-cleaned-up-safely-after-a-r.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-15-earlier-screen-time-stays-attached-when-a-recording-takes-lo.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-15-earlier-screen-time-stays-available-while-you-choose-a-windo.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-15-earlier-screen-time-stays-tied-to-the-device-that-recorded-t.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-15-failed-recordings-no-longer-leave-an-unfinished-earlier-scre.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-15-failed-rewind-cleanup-retries-after-restart.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-15-people-using-a-screen-reader-can-hear-what-none-means-before.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-15-rewind-explains-what-screen-history-agents-can-receive-and-w.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-15-you-can-delete-saved-rewind-history-in-settings-even-when-re.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-16-earlier-screen-time-recovers-from-temporary-failures.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-16-earlier-screen-time-requests-now-recover-after-temporary-rec.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-16-earlier-screen-time-retries.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +500 -99
- package/corpus/templates/clips/desktop/src/lib/clips-action.ts +80 -38
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +271 -89
- package/corpus/templates/clips/desktop/src/lib/recording-preflight.ts +0 -34
- package/corpus/templates/clips/desktop/src/lookback/LookbackRow.tsx +14 -2
- package/corpus/templates/clips/desktop/src/lookback/context-api.ts +175 -26
- package/corpus/templates/clips/desktop/src/lookback/pending-lookback-cleanup-intent.ts +384 -0
- package/corpus/templates/clips/desktop/src/lookback/pending-lookback-context.ts +259 -0
- package/corpus/templates/clips/desktop/src/lookback/pending-lookback-removal.ts +231 -0
- package/corpus/templates/clips/desktop/src/lookback/use-lookback.ts +1 -1
- package/corpus/templates/clips/desktop/src/lookback/worker.ts +229 -18
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +17 -5
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +2 -1
- package/corpus/templates/clips/server/db/schema.ts +2 -0
- package/corpus/templates/clips/server/lib/migration-order.ts +5 -0
- package/corpus/templates/clips/server/lib/recording-context-test-db.ts +4 -2
- package/corpus/templates/clips/server/plugins/db.ts +10 -2
- package/corpus/templates/content/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/crm/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +49 -33
- package/corpus/templates/design/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +24 -0
- package/corpus/templates/design/.generated/bridge/motion-preview.generated.ts +91 -8
- package/corpus/templates/design/CHANGELOG.md +3 -0
- package/corpus/templates/design/actions/apply-motion-edit.ts +167 -26
- package/corpus/templates/design/actions/get-motion-timeline.ts +10 -6
- package/corpus/templates/design/app/components/design/AnimationStyleBrowser.tsx +188 -27
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +34 -9
- package/corpus/templates/design/app/components/design/EditPanel.tsx +37 -6
- package/corpus/templates/design/app/components/design/MotionDock.tsx +235 -78
- package/corpus/templates/design/app/components/design/MotionNativeStyleEditor.tsx +167 -83
- package/corpus/templates/design/app/components/design/MotionTextAnimationEditor.tsx +192 -33
- package/corpus/templates/design/app/components/design/bridge/motion-preview.bridge.ts +115 -6
- package/corpus/templates/design/app/components/design/design-canvas/motion-path-edit.ts +21 -2
- package/corpus/templates/design/app/components/design/design-canvas/motion-types.ts +8 -3
- package/corpus/templates/design/app/components/design/types.ts +1 -0
- package/corpus/templates/design/app/i18n/en-US.ts +60 -5
- package/corpus/templates/design/app/lib/design-import.ts +35 -0
- package/corpus/templates/design/app/pages/design-editor/clone-and-pen-edit.ts +11 -0
- package/corpus/templates/design/app/pages/design-editor/command-types.ts +2 -0
- package/corpus/templates/design/app/pages/design-editor/commands/copy-selection.ts +3 -0
- package/corpus/templates/design/app/pages/design-editor/commands/get-selected-layer-snapshots.ts +19 -0
- package/corpus/templates/design/app/pages/design-editor/commands/paste-selection.ts +118 -20
- package/corpus/templates/design/app/pages/design-editor/commands/toggle-motion-keyframe.ts +15 -4
- package/corpus/templates/design/app/pages/design-editor/design-editor-view.tsx +110 -45
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-content-and-components.ts +228 -16
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-layer-actions.ts +15 -3
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-live-edits-and-presence.ts +49 -3
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-screen-rendering.tsx +6 -1
- package/corpus/templates/design/app/pages/design-editor/effects/motion-autosave.ts +18 -2
- package/corpus/templates/design/app/pages/design-editor/motion-animation-style.ts +1 -7
- package/corpus/templates/design/app/pages/design-editor/motion-state.ts +128 -50
- package/corpus/templates/design/app/pages/design-editor/relative-position-offset.ts +109 -0
- package/corpus/templates/design/app/pages/design-editor/view/motion-dock-panel.tsx +20 -22
- package/corpus/templates/design/app/pages/design-editor/view/single-screen-canvas.tsx +4 -1
- package/corpus/templates/design/changelog/2026-10-14-legacy-text-animations-keep-their-timing-and-remain-editable.md +6 -0
- package/corpus/templates/design/changelog/2026-10-14-text-animation-edits-preserve-collapsed-whitespace-and-reset.md +6 -0
- package/corpus/templates/design/changelog/2026-10-14-text-animations-keep-whitespace-defaults-consistent-hide-irr.md +6 -0
- package/corpus/templates/design/changelog/2026-10-14-text-animations-preserve-legacy-space-handling-and-outgoing-.md +6 -0
- package/corpus/templates/design/changelog/2026-10-14-text-layers-show-one-text-option-for-adding-staggered-motion.md +6 -0
- package/corpus/templates/design/changelog/2026-10-14-text-motion-clears-ignore-spaces-when-the-selected-grouping-.md +6 -0
- package/corpus/templates/design/changelog/2026-10-14-text-motion-edits-now-preserve-saved-stagger-timing-and-outg.md +6 -0
- package/corpus/templates/design/changelog/2026-10-14-text-motion-presets-now-include-editable-direction-grouping-.md +6 -0
- package/corpus/templates/design/changelog/2026-10-15-align-the-motion-style-gallery-with-its-inspector.md +6 -0
- package/corpus/templates/design/changelog/2026-10-15-animation-controls-align-with-the-inspector-edge.md +6 -0
- package/corpus/templates/design/changelog/2026-10-15-animation-recipes-preserve-their-full-duration-at-the-timeli.md +6 -0
- package/corpus/templates/design/changelog/2026-10-15-animation-style-browser-now-aligns-beside-the-inspector.md +6 -0
- package/corpus/templates/design/changelog/2026-10-15-applying-an-animation-recipe-appends-tracks-to-properties-th.md +6 -0
- package/corpus/templates/design/changelog/2026-10-15-dragged-layers-use-the-final-release-position.md +6 -0
- package/corpus/templates/design/changelog/2026-10-15-motion-can-import-and-edit-authored-css-animations-that-chan.md +6 -0
- package/corpus/templates/design/changelog/2026-10-15-motion-duration-edits-stay-stable-when-the-timeline-updates.md +6 -0
- package/corpus/templates/design/changelog/2026-10-15-motion-lets-you-edit-repeated-animation-tracks-independently.md +6 -0
- package/corpus/templates/design/changelog/2026-10-15-motion-now-rejects-html-edits-with-unread-linked-stylesheets.md +6 -0
- package/corpus/templates/design/changelog/2026-10-15-motion-saves-no-longer-conflict-when-the-editor-already-hold.md +6 -0
- package/corpus/templates/design/changelog/2026-10-15-motion-style-popovers-align-with-the-inspector-and-the-galle.md +6 -0
- package/corpus/templates/design/changelog/2026-10-15-repeated-pastes-preserve-relative-layer-offsets.md +6 -0
- package/corpus/templates/design/changelog/2026-10-15-text-animation-edits-keep-long-stagger-timings-aligned-with-.md +6 -0
- package/corpus/templates/design/changelog/2026-10-15-text-stagger-previews-finish-in-sync-with-the-motion-timelin.md +6 -0
- package/corpus/templates/design/changelog/2026-10-15-text-wrapping-hug-widths.md +6 -0
- package/corpus/templates/design/changelog/2026-10-15-the-animation-gallery-lists-only-available-actions.md +6 -0
- package/corpus/templates/design/server/source-workspace.ts +6 -1
- package/corpus/templates/design/shared/code-layer.ts +2 -1
- package/corpus/templates/design/shared/motion-authored-css-import.ts +73 -46
- package/corpus/templates/design/shared/motion-built-in-recipes.ts +610 -0
- package/corpus/templates/design/shared/motion-code.ts +269 -67
- package/corpus/templates/design/shared/motion-compiler.ts +159 -12
- package/corpus/templates/design/shared/motion-html-source.ts +111 -1
- package/corpus/templates/design/shared/motion-native-style-runtime.ts +523 -55
- package/corpus/templates/design/shared/motion-presets.ts +2 -21
- package/corpus/templates/design/shared/motion-react-source.ts +62 -1
- package/corpus/templates/design/shared/motion-text-code.ts +458 -47
- package/corpus/templates/design/shared/motion-timeline.ts +118 -7
- package/corpus/templates/design/shared/relative-position-containing-block.ts +40 -0
- package/corpus/templates/dispatch/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/forms/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/mail/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/mail/actions/search-emails.ts +34 -15
- package/corpus/templates/mail/app/components/email/EmailList.tsx +17 -10
- package/corpus/templates/mail/app/components/email/EmailThread.tsx +80 -30
- package/corpus/templates/mail/app/components/layout/SearchBar.tsx +77 -17
- package/corpus/templates/mail/app/hooks/use-emails.ts +20 -4
- package/corpus/templates/mail/app/lib/mail-routes.ts +19 -0
- package/corpus/templates/mail/app/lib/threads.ts +11 -0
- package/corpus/templates/mail/app/pages/InboxPage.tsx +47 -29
- package/corpus/templates/mail/changelog/2026-10-16-search-surfaces-cached-inbox-matches-sooner-and-uses-fewer-g.md +6 -0
- package/corpus/templates/mail/server/handlers/emails.ts +3 -1
- package/corpus/templates/plan/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/slides/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/slides/AGENTS.md +2 -1
- package/corpus/templates/slides/CHANGELOG.md +1 -0
- package/corpus/templates/slides/actions/view-screen.ts +23 -0
- package/corpus/templates/slides/app/components/editor/BlockBubbleMenu.tsx +3 -5
- package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +103 -79
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +1 -1
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +25 -22
- package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +137 -118
- package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +1154 -924
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +142 -92
- package/corpus/templates/slides/app/components/editor/SlidesLayersPanel.tsx +101 -65
- package/corpus/templates/slides/app/components/editor/rich-text-selection.ts +21 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +34 -0
- package/corpus/templates/slides/app/lib/editor-agent-handoff.ts +39 -0
- package/corpus/templates/slides/app/lib/utils.ts +7 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +263 -93
- package/corpus/templates/slides/changelog/2026-10-16-contextual-toolbar-the-second-row-now-changes-with-what-is-s.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-16-search-and-preview-reference-decks-before-choosing-one.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-16-slides-can-research-public-sources-when-creating-presentatio.md +6 -0
- package/corpus/templates/slides/server/plugins/agent-chat.ts +2 -0
- package/package.json +1 -1
- package/corpus/templates/design/shared/motion-animation-presets.ts +0 -51
package/corpus/README.md
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// AUTO-GENERATED by @agent-native/core deploy build
|
|
2
|
+
import { markDefaultPluginProvided } from "@agent-native/core/server/edge";
|
|
3
|
+
|
|
4
|
+
const pluginStems = ["agent-chat","analytics-alert-jobs","analytics-rollup-backfill-jobs","auth","collab","config","core-routes","creative-context","dashboard-report-jobs","db","feature-flags","labs","onboarding","otel","session-replay-retention-jobs","transactional-emails","uptime-monitor-jobs"];
|
|
5
|
+
|
|
6
|
+
export default function markBuildDiscoveredPlugins(nitroApp) {
|
|
7
|
+
for (const stem of pluginStems) {
|
|
8
|
+
markDefaultPluginProvided(nitroApp, stem);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -373,6 +373,15 @@ function collectDashboardConfigIssues(
|
|
|
373
373
|
}
|
|
374
374
|
}
|
|
375
375
|
}
|
|
376
|
+
// isDashboardFilter rejects a non-boolean customRange, so a save that passes here would drop the whole filter set on read.
|
|
377
|
+
if (
|
|
378
|
+
f.customRange !== undefined &&
|
|
379
|
+
(typeof f.customRange !== "boolean" || f.type !== "select")
|
|
380
|
+
) {
|
|
381
|
+
return dashboardIssue(
|
|
382
|
+
`config.filters[${i}].customRange must be a boolean, and only select filters take it`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
376
385
|
if (f.presets !== undefined) {
|
|
377
386
|
// isDashboardFilter rejects a malformed preset, so a save that passes here would drop the whole filter set on read.
|
|
378
387
|
if (f.type !== "multi-select" || !Array.isArray(f.presets)) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useSession } from "@agent-native/core/client/hooks";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
|
|
4
|
+
import { dashboardCacheScope } from "@/lib/prefetch-keys";
|
|
5
|
+
import { syncSqlQuerySnapshotsToSession } from "@/lib/sql-query-snapshot-session";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Keeps saved SQL results tied to the signed-in account. Mount it outside
|
|
9
|
+
* `RequireSession`: that gate unmounts everything under it as soon as a
|
|
10
|
+
* sign-out begins, so a cleanup effect inside it never sees the new status.
|
|
11
|
+
* Unmounting this component clears nothing, which is what lets a reload or an
|
|
12
|
+
* ordinary navigation keep the results it is about to restore.
|
|
13
|
+
*/
|
|
14
|
+
export function SqlQuerySnapshotSessionBridge() {
|
|
15
|
+
const { session, status } = useSession();
|
|
16
|
+
const scope = session ? dashboardCacheScope(session) : null;
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
syncSqlQuerySnapshotsToSession({ status, session });
|
|
20
|
+
// `scope` stands for the account: the session object changes identity on
|
|
21
|
+
// every re-read without changing who is signed in.
|
|
22
|
+
}, [status, scope]);
|
|
23
|
+
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { useFormatters, useT } from "@agent-native/core/client/i18n";
|
|
2
|
+
import { IconCheck, IconClock } from "@tabler/icons-react";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Tooltip,
|
|
7
|
+
TooltipContent,
|
|
8
|
+
TooltipTrigger,
|
|
9
|
+
} from "@/components/ui/tooltip";
|
|
10
|
+
import { cn } from "@/lib/utils";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
describeFreshnessText,
|
|
14
|
+
relativeFetchAge,
|
|
15
|
+
type PanelFreshness,
|
|
16
|
+
} from "./panel-freshness";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* When the panel's data was fetched and whether it came from a cache, behind a
|
|
20
|
+
* clock in the panel's hover actions. The tooltip opens on hover and focus; a
|
|
21
|
+
* tap opens it too, because a tooltip alone never opens on touch.
|
|
22
|
+
*/
|
|
23
|
+
export function PanelFreshnessClock({
|
|
24
|
+
freshness,
|
|
25
|
+
className,
|
|
26
|
+
showAge = false,
|
|
27
|
+
}: {
|
|
28
|
+
freshness: PanelFreshness;
|
|
29
|
+
className?: string;
|
|
30
|
+
showAge?: boolean;
|
|
31
|
+
}) {
|
|
32
|
+
const t = useT();
|
|
33
|
+
const formatters = useFormatters();
|
|
34
|
+
const [open, setOpen] = useState(false);
|
|
35
|
+
const [now, setNow] = useState(Date.now);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!showAge || !freshness.fetchedAt) return;
|
|
38
|
+
const timer = setTimeout(
|
|
39
|
+
() => setNow(Date.now()),
|
|
40
|
+
60_000 - (Date.now() % 60_000),
|
|
41
|
+
);
|
|
42
|
+
return () => clearTimeout(timer);
|
|
43
|
+
}, [showAge, freshness.fetchedAt, now]);
|
|
44
|
+
const status = freshness.status;
|
|
45
|
+
const updating =
|
|
46
|
+
status === "updating" || status === "previous-filters-updating";
|
|
47
|
+
const statusText =
|
|
48
|
+
status === "previous-filters-updating"
|
|
49
|
+
? t("sqlDashboard.updatingPreviousFilters")
|
|
50
|
+
: status === "updating"
|
|
51
|
+
? t("sqlDashboard.updating")
|
|
52
|
+
: status === "previous-filters"
|
|
53
|
+
? t("sqlDashboard.previousFilters")
|
|
54
|
+
: status === "refreshed"
|
|
55
|
+
? t("sqlDashboard.refreshed")
|
|
56
|
+
: undefined;
|
|
57
|
+
const label = [
|
|
58
|
+
statusText,
|
|
59
|
+
describeFreshnessText(t, formatters, freshness, "full"),
|
|
60
|
+
]
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.join(" · ");
|
|
63
|
+
const age =
|
|
64
|
+
showAge && freshness.fetchedAt
|
|
65
|
+
? relativeFetchAge(
|
|
66
|
+
formatters,
|
|
67
|
+
freshness.fetchedAt,
|
|
68
|
+
Math.max(now, Date.now()),
|
|
69
|
+
)
|
|
70
|
+
: undefined;
|
|
71
|
+
const visibleText =
|
|
72
|
+
statusText ??
|
|
73
|
+
(age
|
|
74
|
+
? t("sqlDashboard.updatedAgo", { age })
|
|
75
|
+
: status === "stale"
|
|
76
|
+
? describeFreshnessText(t, formatters, freshness, "compact")
|
|
77
|
+
: undefined);
|
|
78
|
+
return (
|
|
79
|
+
<Tooltip open={open} onOpenChange={setOpen}>
|
|
80
|
+
<TooltipTrigger asChild>
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
className={cn(
|
|
84
|
+
"panel-freshness-clock inline-flex min-w-0 max-w-full items-center justify-end gap-1.5 whitespace-nowrap rounded text-xs text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
85
|
+
!status && "panel-freshness-routine",
|
|
86
|
+
className,
|
|
87
|
+
)}
|
|
88
|
+
aria-label={label}
|
|
89
|
+
data-dashboard-report-freshness={freshness.cache}
|
|
90
|
+
data-dashboard-report-updating={
|
|
91
|
+
updating
|
|
92
|
+
? status === "previous-filters-updating"
|
|
93
|
+
? "previous-filters"
|
|
94
|
+
: "refresh"
|
|
95
|
+
: undefined
|
|
96
|
+
}
|
|
97
|
+
data-dashboard-report-paused={
|
|
98
|
+
status === "previous-filters" ? "previous-filters" : undefined
|
|
99
|
+
}
|
|
100
|
+
data-dashboard-report-refreshed={
|
|
101
|
+
status === "refreshed" ? "" : undefined
|
|
102
|
+
}
|
|
103
|
+
data-dashboard-report-stale={
|
|
104
|
+
status === "stale" ? freshness.cache : undefined
|
|
105
|
+
}
|
|
106
|
+
onClick={(event) => {
|
|
107
|
+
event.preventDefault();
|
|
108
|
+
setOpen(true);
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
<span
|
|
112
|
+
className="panel-freshness-text min-w-0 truncate"
|
|
113
|
+
aria-hidden="true"
|
|
114
|
+
>
|
|
115
|
+
{status === "stale" && !showAge ? (
|
|
116
|
+
<time dateTime={freshness.fetchedAt}>{visibleText}</time>
|
|
117
|
+
) : (
|
|
118
|
+
visibleText
|
|
119
|
+
)}
|
|
120
|
+
</span>
|
|
121
|
+
<span className="sr-only" role="status">
|
|
122
|
+
{statusText}
|
|
123
|
+
</span>
|
|
124
|
+
{status === "refreshed" ? (
|
|
125
|
+
<IconCheck aria-hidden="true" className="size-3.5 shrink-0" />
|
|
126
|
+
) : (
|
|
127
|
+
<IconClock
|
|
128
|
+
aria-hidden="true"
|
|
129
|
+
className={cn(
|
|
130
|
+
"size-3.5 shrink-0",
|
|
131
|
+
updating && "animate-spin motion-reduce:animate-none",
|
|
132
|
+
)}
|
|
133
|
+
/>
|
|
134
|
+
)}
|
|
135
|
+
</button>
|
|
136
|
+
</TooltipTrigger>
|
|
137
|
+
<TooltipContent>{label}</TooltipContent>
|
|
138
|
+
</Tooltip>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
@@ -114,7 +114,6 @@ import {
|
|
|
114
114
|
SelectTrigger,
|
|
115
115
|
SelectValue,
|
|
116
116
|
} from "@/components/ui/select";
|
|
117
|
-
import { Spinner } from "@/components/ui/spinner";
|
|
118
117
|
import {
|
|
119
118
|
Tooltip as HoverTooltip,
|
|
120
119
|
TooltipContent as HoverTooltipContent,
|
|
@@ -157,6 +156,12 @@ import {
|
|
|
157
156
|
useEventMarkerAxisPadding,
|
|
158
157
|
useEventMarkerGroups,
|
|
159
158
|
} from "./event-markers";
|
|
159
|
+
import {
|
|
160
|
+
useIsStaleResult,
|
|
161
|
+
useRefreshConfirmation,
|
|
162
|
+
type PanelFreshness,
|
|
163
|
+
} from "./panel-freshness";
|
|
164
|
+
import { PanelFreshnessClock } from "./PanelFreshnessClock";
|
|
160
165
|
|
|
161
166
|
export { limitChartRows };
|
|
162
167
|
|
|
@@ -1266,6 +1271,11 @@ interface SqlChartProps {
|
|
|
1266
1271
|
refreshToken?: number;
|
|
1267
1272
|
onRefreshRequested?: () => void;
|
|
1268
1273
|
onExportCsvChange?: (handler: (() => void) | null) => void;
|
|
1274
|
+
/**
|
|
1275
|
+
* The freshness of the result on screen, for a host that shows it outside
|
|
1276
|
+
* the chart; null when no result is shown, and again when the chart unmounts.
|
|
1277
|
+
*/
|
|
1278
|
+
onFreshnessChange?: (freshness: PanelFreshness | null) => void;
|
|
1269
1279
|
onCopyTableChange?: (handler: (() => Promise<void>) | null) => void;
|
|
1270
1280
|
/** Passing this makes a table's columns editable; omit it for read-only tables. */
|
|
1271
1281
|
onSaveColumns?: (columns: TableColumnConfig[]) => void;
|
|
@@ -1292,6 +1302,7 @@ export function SqlChart({
|
|
|
1292
1302
|
refreshToken = 0,
|
|
1293
1303
|
onRefreshRequested,
|
|
1294
1304
|
onExportCsvChange,
|
|
1305
|
+
onFreshnessChange,
|
|
1295
1306
|
onCopyTableChange,
|
|
1296
1307
|
onSaveColumns,
|
|
1297
1308
|
columnWidths,
|
|
@@ -1333,6 +1344,8 @@ export function SqlChart({
|
|
|
1333
1344
|
isFetching: queryIsFetching,
|
|
1334
1345
|
isPlaceholderData: queryIsPlaceholderData,
|
|
1335
1346
|
error: queryError,
|
|
1347
|
+
freshness: queryFreshness,
|
|
1348
|
+
completedManualRefreshToken,
|
|
1336
1349
|
} = useSqlQuery(
|
|
1337
1350
|
["sql-chart", dashboardId || panel.id, sql, panel.source],
|
|
1338
1351
|
sql,
|
|
@@ -1344,6 +1357,7 @@ export function SqlChart({
|
|
|
1344
1357
|
retainPreviousScope,
|
|
1345
1358
|
retainWhilePaused: retainWhileOffscreen,
|
|
1346
1359
|
reuseCompletedOnEnable: retainWhileOffscreen,
|
|
1360
|
+
snapshots: !isSection && !isExtension && !resultOverride,
|
|
1347
1361
|
},
|
|
1348
1362
|
);
|
|
1349
1363
|
|
|
@@ -1409,6 +1423,59 @@ export function SqlChart({
|
|
|
1409
1423
|
[chartType, rows, xKey],
|
|
1410
1424
|
);
|
|
1411
1425
|
|
|
1426
|
+
const showsRetainedResult = retainWhileOffscreen && queryResult !== undefined;
|
|
1427
|
+
const showsSkeleton =
|
|
1428
|
+
isLoading ||
|
|
1429
|
+
isFetching ||
|
|
1430
|
+
(!loadData && showLoadingWhenDisabled && !showsRetainedResult);
|
|
1431
|
+
// A result handed in by the caller was not fetched by this panel's query.
|
|
1432
|
+
const shownFreshness = resultOverride ? undefined : queryFreshness;
|
|
1433
|
+
const isStale = useIsStaleResult(shownFreshness?.fetchedAt);
|
|
1434
|
+
const refreshConfirmed = useRefreshConfirmation({
|
|
1435
|
+
requestToken: refreshToken + localRefreshToken,
|
|
1436
|
+
scopeKey: JSON.stringify([
|
|
1437
|
+
dashboardId ?? null,
|
|
1438
|
+
panel.id,
|
|
1439
|
+
panel.source,
|
|
1440
|
+
sql,
|
|
1441
|
+
]),
|
|
1442
|
+
completedToken: completedManualRefreshToken,
|
|
1443
|
+
});
|
|
1444
|
+
const canReportFreshness =
|
|
1445
|
+
!isSection && !isExtension && !resultOverride && !error && !showsSkeleton;
|
|
1446
|
+
const reportedCache = shownFreshness?.cache;
|
|
1447
|
+
const reportedFetchedAt = shownFreshness?.fetchedAt;
|
|
1448
|
+
const reportedStatus: PanelFreshness["status"] = isUpdating
|
|
1449
|
+
? isShowingPreviousFilters
|
|
1450
|
+
? "previous-filters-updating"
|
|
1451
|
+
: "updating"
|
|
1452
|
+
: isPausedOnPreviousFilters
|
|
1453
|
+
? "previous-filters"
|
|
1454
|
+
: refreshConfirmed
|
|
1455
|
+
? "refreshed"
|
|
1456
|
+
: isStale
|
|
1457
|
+
? "stale"
|
|
1458
|
+
: undefined;
|
|
1459
|
+
useEffect(() => {
|
|
1460
|
+
if (!onFreshnessChange) return;
|
|
1461
|
+
onFreshnessChange(
|
|
1462
|
+
canReportFreshness && (reportedCache || reportedStatus)
|
|
1463
|
+
? {
|
|
1464
|
+
fetchedAt: reportedFetchedAt,
|
|
1465
|
+
cache: reportedCache ?? "unknown",
|
|
1466
|
+
...(reportedStatus ? { status: reportedStatus } : {}),
|
|
1467
|
+
}
|
|
1468
|
+
: null,
|
|
1469
|
+
);
|
|
1470
|
+
return () => onFreshnessChange(null);
|
|
1471
|
+
}, [
|
|
1472
|
+
onFreshnessChange,
|
|
1473
|
+
canReportFreshness,
|
|
1474
|
+
reportedCache,
|
|
1475
|
+
reportedFetchedAt,
|
|
1476
|
+
reportedStatus,
|
|
1477
|
+
]);
|
|
1478
|
+
|
|
1412
1479
|
if (isSection) {
|
|
1413
1480
|
return (
|
|
1414
1481
|
<div className="px-1 py-2">
|
|
@@ -1462,12 +1529,7 @@ export function SqlChart({
|
|
|
1462
1529
|
: "min-h-[250px]";
|
|
1463
1530
|
const placeholderPadY = isMetric ? "py-2" : "py-8";
|
|
1464
1531
|
|
|
1465
|
-
|
|
1466
|
-
if (
|
|
1467
|
-
isLoading ||
|
|
1468
|
-
isFetching ||
|
|
1469
|
-
(!loadData && showLoadingWhenDisabled && !showsRetainedResult)
|
|
1470
|
-
) {
|
|
1532
|
+
if (showsSkeleton) {
|
|
1471
1533
|
return <SqlChartLoadingSkeleton panel={panel} />;
|
|
1472
1534
|
}
|
|
1473
1535
|
|
|
@@ -1496,33 +1558,6 @@ export function SqlChart({
|
|
|
1496
1558
|
}
|
|
1497
1559
|
|
|
1498
1560
|
const missingConfigKeys = plan.missingKeys;
|
|
1499
|
-
const updatingNotice = isUpdating ? (
|
|
1500
|
-
<div className="pointer-events-none relative h-0">
|
|
1501
|
-
<span
|
|
1502
|
-
role="status"
|
|
1503
|
-
data-dashboard-report-updating={
|
|
1504
|
-
isShowingPreviousFilters ? "previous-filters" : "refresh"
|
|
1505
|
-
}
|
|
1506
|
-
className="absolute right-0 top-0 z-10 inline-flex items-center gap-1.5 rounded-full border bg-background/90 px-2 py-0.5 text-xs text-muted-foreground shadow-sm"
|
|
1507
|
-
>
|
|
1508
|
-
<Spinner aria-hidden="true" className="size-3" />
|
|
1509
|
-
{t(
|
|
1510
|
-
isShowingPreviousFilters
|
|
1511
|
-
? "sqlDashboard.updatingPreviousFilters"
|
|
1512
|
-
: "sqlDashboard.updating",
|
|
1513
|
-
)}
|
|
1514
|
-
</span>
|
|
1515
|
-
</div>
|
|
1516
|
-
) : isPausedOnPreviousFilters ? (
|
|
1517
|
-
<div className="pointer-events-none relative h-0">
|
|
1518
|
-
<span
|
|
1519
|
-
data-dashboard-report-paused="previous-filters"
|
|
1520
|
-
className="absolute right-0 top-0 z-10 inline-flex items-center rounded-full border bg-background/90 px-2 py-0.5 text-xs text-muted-foreground shadow-sm"
|
|
1521
|
-
>
|
|
1522
|
-
{t("sqlDashboard.previousFilters")}
|
|
1523
|
-
</span>
|
|
1524
|
-
</div>
|
|
1525
|
-
) : null;
|
|
1526
1561
|
const withConfigBanners = (node: ReactNode) => {
|
|
1527
1562
|
if (refreshError) {
|
|
1528
1563
|
return (
|
|
@@ -1567,12 +1602,38 @@ export function SqlChart({
|
|
|
1567
1602
|
);
|
|
1568
1603
|
};
|
|
1569
1604
|
|
|
1570
|
-
const
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1605
|
+
const showStandaloneStatus =
|
|
1606
|
+
!onFreshnessChange &&
|
|
1607
|
+
(shownFreshness !== undefined || reportedStatus !== undefined);
|
|
1608
|
+
const withNotice = (node: ReactNode) => (
|
|
1609
|
+
<div
|
|
1610
|
+
className={
|
|
1611
|
+
showStandaloneStatus
|
|
1612
|
+
? "grid h-full min-h-0 grid-cols-[minmax(0,1fr)_1.5rem] items-start gap-x-1"
|
|
1613
|
+
: "flex h-full min-h-0 flex-col"
|
|
1614
|
+
}
|
|
1615
|
+
>
|
|
1616
|
+
<div data-dashboard-report-chart-body className="h-full min-h-0 min-w-0">
|
|
1617
|
+
{node}
|
|
1618
|
+
</div>
|
|
1619
|
+
{showStandaloneStatus ? (
|
|
1620
|
+
<div data-dashboard-report-standalone-status className="min-w-0">
|
|
1621
|
+
<HoverTooltipProvider>
|
|
1622
|
+
<PanelFreshnessClock
|
|
1623
|
+
freshness={{
|
|
1624
|
+
cache: "unknown",
|
|
1625
|
+
...shownFreshness,
|
|
1626
|
+
status: reportedStatus,
|
|
1627
|
+
}}
|
|
1628
|
+
className="h-6 w-6 p-1"
|
|
1629
|
+
/>
|
|
1630
|
+
</HoverTooltipProvider>
|
|
1631
|
+
</div>
|
|
1632
|
+
) : null}
|
|
1633
|
+
</div>
|
|
1575
1634
|
);
|
|
1635
|
+
const withConfigWarning = (node: ReactNode) =>
|
|
1636
|
+
withNotice(withConfigBanners(node));
|
|
1576
1637
|
|
|
1577
1638
|
const pointLimit = plan.pointLimit;
|
|
1578
1639
|
const withPointLimit = (node: ReactNode) =>
|
|
@@ -1588,7 +1649,7 @@ export function SqlChart({
|
|
|
1588
1649
|
if (plan.empty) {
|
|
1589
1650
|
const noData = (
|
|
1590
1651
|
<div
|
|
1591
|
-
className={`flex flex-1 items-center justify-center ${placeholderPadY} ${placeholderMinH}`}
|
|
1652
|
+
className={`flex h-full flex-1 items-center justify-center ${placeholderPadY} ${placeholderMinH}`}
|
|
1592
1653
|
>
|
|
1593
1654
|
<p className="text-sm text-muted-foreground text-center">
|
|
1594
1655
|
{t("common.noData")}
|
|
@@ -1598,14 +1659,9 @@ export function SqlChart({
|
|
|
1598
1659
|
// A funnel or heatmap with rows that draws nothing keeps the warning and
|
|
1599
1660
|
// refresh banner that say why; a result with no rows to plot is a bare
|
|
1600
1661
|
// "No data".
|
|
1601
|
-
return plan.rows.length === 0
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
{noData}
|
|
1605
|
-
</>
|
|
1606
|
-
) : (
|
|
1607
|
-
withConfigWarning(noData)
|
|
1608
|
-
);
|
|
1662
|
+
return plan.rows.length === 0
|
|
1663
|
+
? withNotice(noData)
|
|
1664
|
+
: withConfigWarning(noData);
|
|
1609
1665
|
}
|
|
1610
1666
|
|
|
1611
1667
|
if (chartType === "metric") {
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { useFormatters, useT } from "@agent-native/core/client/i18n";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
isStaleFetchTime,
|
|
6
|
+
msUntilStale,
|
|
7
|
+
type SqlQueryFreshness,
|
|
8
|
+
} from "@/lib/sql-query-freshness";
|
|
9
|
+
|
|
10
|
+
/** How long a panel's own successful refresh says so before the text goes. */
|
|
11
|
+
export const REFRESH_CONFIRMATION_MS = 3000;
|
|
12
|
+
|
|
13
|
+
// setTimeout stores its delay in 32 bits; a longer one fires immediately.
|
|
14
|
+
const MAX_TIMEOUT_MS = 2 ** 31 - 1;
|
|
15
|
+
|
|
16
|
+
type Translate = ReturnType<typeof useT>;
|
|
17
|
+
type Formatters = Pick<ReturnType<typeof useFormatters>, "formatDate">;
|
|
18
|
+
|
|
19
|
+
export type PanelFreshness = SqlQueryFreshness & {
|
|
20
|
+
status?:
|
|
21
|
+
| "updating"
|
|
22
|
+
| "previous-filters-updating"
|
|
23
|
+
| "previous-filters"
|
|
24
|
+
| "refreshed"
|
|
25
|
+
| "stale";
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function relativeFetchAge(
|
|
29
|
+
formatters: Pick<ReturnType<typeof useFormatters>, "formatRelativeTime">,
|
|
30
|
+
fetchedAt: string,
|
|
31
|
+
now: number,
|
|
32
|
+
): string | undefined {
|
|
33
|
+
const age = now - Date.parse(fetchedAt);
|
|
34
|
+
if (!Number.isFinite(age) || age < 0) return undefined;
|
|
35
|
+
const minutes = Math.floor(age / 60_000);
|
|
36
|
+
const [value, unit] =
|
|
37
|
+
minutes < 60
|
|
38
|
+
? [minutes, "minute" as const]
|
|
39
|
+
: minutes < 1440
|
|
40
|
+
? [Math.floor(minutes / 60), "hour" as const]
|
|
41
|
+
: [Math.floor(minutes / 1440), "day" as const];
|
|
42
|
+
return formatters.formatRelativeTime(-value, unit, {
|
|
43
|
+
style: "narrow",
|
|
44
|
+
numeric: "always",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The time alone for today's fetch, with the date once it is older. */
|
|
49
|
+
function fetchedTimeFormat(fetchedAt: string): Intl.DateTimeFormatOptions {
|
|
50
|
+
const fetched = new Date(fetchedAt);
|
|
51
|
+
const now = new Date();
|
|
52
|
+
const today =
|
|
53
|
+
fetched.getFullYear() === now.getFullYear() &&
|
|
54
|
+
fetched.getMonth() === now.getMonth() &&
|
|
55
|
+
fetched.getDate() === now.getDate();
|
|
56
|
+
return today
|
|
57
|
+
? { hour: "numeric", minute: "2-digit" }
|
|
58
|
+
: { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* "Fetched 5:20 PM · Cached", or the full timestamp for the clock's tooltip.
|
|
63
|
+
* A missing fetch time or cache state says so instead of being left out.
|
|
64
|
+
*/
|
|
65
|
+
export function describeFreshnessText(
|
|
66
|
+
t: Translate,
|
|
67
|
+
formatters: Formatters,
|
|
68
|
+
{ fetchedAt, cache }: SqlQueryFreshness,
|
|
69
|
+
detail: "compact" | "full",
|
|
70
|
+
): string {
|
|
71
|
+
const parts = [
|
|
72
|
+
fetchedAt
|
|
73
|
+
? t("sqlDashboard.fetchedAt", {
|
|
74
|
+
time: formatters.formatDate(
|
|
75
|
+
fetchedAt,
|
|
76
|
+
detail === "full"
|
|
77
|
+
? { dateStyle: "long", timeStyle: "medium" }
|
|
78
|
+
: fetchedTimeFormat(fetchedAt),
|
|
79
|
+
),
|
|
80
|
+
})
|
|
81
|
+
: t("sqlDashboard.fetchTimeUnknown"),
|
|
82
|
+
];
|
|
83
|
+
if (cache === "app-cache") parts.push(t("sqlDashboard.cacheApp"));
|
|
84
|
+
else if (cache === "provider-cache")
|
|
85
|
+
parts.push(t("sqlDashboard.cacheProvider"));
|
|
86
|
+
else if (cache === "restored") parts.push(t("sqlDashboard.cacheRestored"));
|
|
87
|
+
else if (cache === "unknown") parts.push(t("sqlDashboard.cacheUnknown"));
|
|
88
|
+
return parts.join(" · ");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Whether the shown result is older than the cutoff, read from the clock on
|
|
93
|
+
* every render. A result that is not yet that old schedules one timeout for
|
|
94
|
+
* the moment it will be, so an idle panel still turns stale without polling,
|
|
95
|
+
* a shared timer, or a request; the timeout re-renders only this panel, once.
|
|
96
|
+
*/
|
|
97
|
+
export function useIsStaleResult(fetchedAt: string | undefined): boolean {
|
|
98
|
+
const [tick, setTick] = useState(0);
|
|
99
|
+
const stale = isStaleFetchTime(fetchedAt, Date.now());
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (stale) return;
|
|
102
|
+
const delay = msUntilStale(fetchedAt, Date.now());
|
|
103
|
+
if (delay === undefined) return;
|
|
104
|
+
const timer = setTimeout(
|
|
105
|
+
() => setTick((count) => count + 1),
|
|
106
|
+
Math.min(delay, MAX_TIMEOUT_MS),
|
|
107
|
+
);
|
|
108
|
+
return () => clearTimeout(timer);
|
|
109
|
+
}, [fetchedAt, stale, tick]);
|
|
110
|
+
return stale;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface RefreshConfirmationInput {
|
|
114
|
+
/** Rises each time the user asks this panel to refresh. */
|
|
115
|
+
requestToken: number;
|
|
116
|
+
/** Changes when the panel's query does, so its refresh can no longer land. */
|
|
117
|
+
scopeKey: string;
|
|
118
|
+
/** The request whose own forced fetch just succeeded, from `useSqlQuery`. */
|
|
119
|
+
completedToken: number | undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* True for `REFRESH_CONFIRMATION_MS` after the fetch of this panel's latest
|
|
124
|
+
* manual refresh request succeeds. Only a completion for exactly the current
|
|
125
|
+
* request counts: an automatic revalidation, a cancelled, failed or replaced
|
|
126
|
+
* request, or a token the panel mounted with never reports one.
|
|
127
|
+
*/
|
|
128
|
+
export function useRefreshConfirmation(input: RefreshConfirmationInput) {
|
|
129
|
+
const { requestToken, scopeKey, completedToken } = input;
|
|
130
|
+
const [confirmedIdentity, setConfirmedIdentity] = useState<string>();
|
|
131
|
+
const processedIdentity = useRef<string | undefined>(undefined);
|
|
132
|
+
const context = useRef({ requestToken, scopeKey });
|
|
133
|
+
const completionIdentity =
|
|
134
|
+
completedToken === requestToken
|
|
135
|
+
? JSON.stringify([requestToken, scopeKey, completedToken])
|
|
136
|
+
: undefined;
|
|
137
|
+
|
|
138
|
+
// A completion already present during a request or scope change belongs to
|
|
139
|
+
// the old context. Consume it so it cannot be adopted by the new panel state.
|
|
140
|
+
if (
|
|
141
|
+
context.current.requestToken !== requestToken ||
|
|
142
|
+
context.current.scopeKey !== scopeKey
|
|
143
|
+
) {
|
|
144
|
+
if (completionIdentity) processedIdentity.current = completionIdentity;
|
|
145
|
+
context.current = { requestToken, scopeKey };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (!completionIdentity) {
|
|
150
|
+
setConfirmedIdentity(undefined);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (processedIdentity.current === completionIdentity) return;
|
|
154
|
+
processedIdentity.current = completionIdentity;
|
|
155
|
+
setConfirmedIdentity(completionIdentity);
|
|
156
|
+
}, [completionIdentity]);
|
|
157
|
+
|
|
158
|
+
const confirmed =
|
|
159
|
+
completionIdentity !== undefined &&
|
|
160
|
+
confirmedIdentity === completionIdentity;
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
if (!confirmed) return;
|
|
163
|
+
const timer = setTimeout(
|
|
164
|
+
() => setConfirmedIdentity(undefined),
|
|
165
|
+
REFRESH_CONFIRMATION_MS,
|
|
166
|
+
);
|
|
167
|
+
return () => clearTimeout(timer);
|
|
168
|
+
}, [confirmed]);
|
|
169
|
+
|
|
170
|
+
return confirmed;
|
|
171
|
+
}
|
|
@@ -610,6 +610,65 @@
|
|
|
610
610
|
opacity: 1;
|
|
611
611
|
}
|
|
612
612
|
|
|
613
|
+
.dashboard-chart-card {
|
|
614
|
+
container-type: inline-size;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/* Status and hover controls keep their tracks when their contents change. */
|
|
618
|
+
.dashboard-panel-header {
|
|
619
|
+
display: grid;
|
|
620
|
+
grid-template-columns: minmax(0, 1fr) 1.5rem 5rem;
|
|
621
|
+
align-items: center;
|
|
622
|
+
column-gap: 0.5rem;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
.dashboard-panel-header > * {
|
|
626
|
+
margin-block: 0;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
.dashboard-panel-freshness {
|
|
630
|
+
display: flex;
|
|
631
|
+
align-items: center;
|
|
632
|
+
height: 1.5rem;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.dashboard-panel-freshness .panel-freshness-routine {
|
|
636
|
+
opacity: 0;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
.dashboard-chart-card:hover .panel-freshness-routine,
|
|
640
|
+
.dashboard-chart-card:focus-within .panel-freshness-routine {
|
|
641
|
+
opacity: 1;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
@media (hover: none) {
|
|
645
|
+
.dashboard-panel-freshness .panel-freshness-routine {
|
|
646
|
+
opacity: 1;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
.panel-freshness-text {
|
|
651
|
+
display: none;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.dashboard-panel-actions {
|
|
655
|
+
height: 1.5rem;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
@container (min-width: 30rem) {
|
|
659
|
+
.dashboard-panel-header {
|
|
660
|
+
grid-template-columns: minmax(0, 1fr) 9rem 5rem;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
.dashboard-panel-freshness .panel-freshness-text {
|
|
664
|
+
display: block;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
.dashboard-panel-freshness .panel-freshness-routine {
|
|
668
|
+
opacity: 1;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
613
672
|
.dashboard-chart-card[data-dragging="true"],
|
|
614
673
|
.dashboard-section-card[data-dragging="true"] {
|
|
615
674
|
opacity: 0.55;
|
|
@@ -943,6 +943,7 @@ export default {
|
|
|
943
943
|
certificationFailed: "Couldn't certify dashboard: {{message}}",
|
|
944
944
|
details: "Details",
|
|
945
945
|
updated: "Updated {{date}}",
|
|
946
|
+
updatedAgo: "Updated {{age}}",
|
|
946
947
|
public: "Public",
|
|
947
948
|
sharedWithOrg: "Shared with org",
|
|
948
949
|
private: "Private",
|
|
@@ -998,8 +999,15 @@ export default {
|
|
|
998
999
|
refresh: "Refresh",
|
|
999
1000
|
refreshing: "Refreshing...",
|
|
1000
1001
|
updating: "Updating",
|
|
1002
|
+
refreshed: "Refreshed",
|
|
1001
1003
|
updatingPreviousFilters: "Previous filters · Updating",
|
|
1002
1004
|
previousFilters: "Previous filters",
|
|
1005
|
+
fetchedAt: "Fetched {{time}}",
|
|
1006
|
+
fetchTimeUnknown: "Fetch time unknown",
|
|
1007
|
+
cacheApp: "Cached",
|
|
1008
|
+
cacheProvider: "BigQuery cache",
|
|
1009
|
+
cacheRestored: "Saved copy",
|
|
1010
|
+
cacheUnknown: "Cache status unknown",
|
|
1003
1011
|
downloadCsv: "Download CSV",
|
|
1004
1012
|
copyTable: "Copy table",
|
|
1005
1013
|
multiColumnSortHelp: "Click to sort. Shift-click to add another sort.",
|