@agent-native/core-corpus 0.202.0 → 0.202.1-nightly-20261007011022
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/actions/list-sql-dashboards.ts +2 -0
- package/corpus/templates/analytics/actions/query-dashboard-panel.ts +12 -3
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +49 -5
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +38 -8
- package/corpus/templates/analytics/app/i18n/en-US.ts +1 -1
- package/corpus/templates/analytics/app/lib/dashboard-visibility.ts +17 -2
- package/corpus/templates/analytics/app/lib/sql-query.ts +60 -8
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +8 -18
- package/corpus/templates/analytics/changelog/2026-10-06-dashboard-filter-mine-now-lists-every-dashboard-you-own-incl.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-06-refreshing-a-bigquery-dashboard-panel-fetches-current-data.md +6 -0
- package/corpus/templates/analytics/server/db/schema.ts +5 -0
- package/corpus/templates/analytics/server/lib/bigquery.ts +500 -133
- package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +7 -2
- package/corpus/templates/analytics/server/lib/dashboard-panel-source-resolver.ts +7 -0
- package/corpus/templates/analytics/server/plugins/db.ts +17 -0
- package/corpus/templates/brain/.agents/skills/ingestion-and-connectors/SKILL.md +7 -2
- package/corpus/templates/brain/changelog/2026-10-06-zoom-transcripts-now-download.md +6 -0
- package/corpus/templates/brain/server/lib/connectors.ts +66 -4
- package/corpus/templates/brain/server/lib/zoom.ts +37 -4
- package/corpus/templates/brain/server/register-secrets.ts +3 -3
- package/corpus/templates/calendar/changelog/2026-10-06-builder-io-setup-details-render-immediately-and-model-settin.md +6 -0
- package/corpus/templates/clips/app/components/player/create-account-dialog.tsx +123 -10
- package/corpus/templates/clips/app/components/player/sign-in-prompt-dialog.tsx +7 -2
- package/corpus/templates/clips/app/components/player/signed-out-share-actions.tsx +11 -2
- package/corpus/templates/clips/app/i18n/en-US.ts +8 -0
- package/corpus/templates/clips/app/routes/access-request.approve.tsx +4 -1
- package/corpus/templates/clips/app/routes/invite.$token.tsx +2 -1
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +12 -8
- package/corpus/templates/clips/changelog/2026-10-06-fixed-returning-to-shared-clips-after-email-verification.md +6 -0
- package/corpus/templates/content/changelog/2026-10-06-generated-documents-from-chatgpt-open-in-the-full-content-ed.md +6 -0
- package/corpus/templates/content/server/lib/chatgpt-directory-tools.ts +0 -2
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +12 -6
- package/corpus/templates/design/actions/generate-design.ts +188 -45
- package/corpus/templates/design/actions/present-design-variants.ts +113 -39
- package/corpus/templates/design/app/components/home/DesignHomeLibrary.tsx +5 -11
- package/corpus/templates/design/app/hooks/use-question-flow.ts +1 -1
- package/corpus/templates/design/app/pages/Index.tsx +57 -13
- package/corpus/templates/design/app/pages/design-editor/derive/overview-screens.ts +10 -2
- package/corpus/templates/design/app/pages/design-editor/geometry-persistence.ts +5 -4
- package/corpus/templates/design/changelog/2026-10-05-design-keeps-exact-size-screens-at-their-requested-dimension.md +6 -0
- package/corpus/templates/design/changelog/2026-10-06-the-recent-tab-stays-visible-while-designs-load-and-opens-im.md +6 -0
- package/corpus/templates/design/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/design/shared/canvas-dimensions.ts +204 -0
- package/corpus/templates/design/shared/responsive-frame-layout.ts +1 -0
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +17 -5
- package/corpus/templates/mail/app/root.tsx +16 -1
- package/corpus/templates/mail/changelog/2026-10-02-pressing-r-on-a-message-in-the-list-opens-the-reply-window-e.md +6 -0
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +5 -2
- package/corpus/templates/plan/app/i18n/en-US.ts +6 -1
- package/corpus/templates/plan/app/pages/PlansPage.tsx +113 -9
- package/corpus/templates/slides/actions/add-slide.ts +3 -3
- package/corpus/templates/slides/actions/get-deck-reference-context.ts +101 -21
- package/corpus/templates/slides/actions/get-design-system.ts +51 -11
- package/corpus/templates/slides/actions/read-composer-source.ts +5 -1
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +4 -1
- package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
- package/corpus/templates/slides/app/lib/composer-context.ts +1 -1
- package/corpus/templates/slides/app/lib/create-deck-generation.ts +133 -34
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +3 -3
- package/corpus/templates/slides/app/pages/Index.tsx +138 -41
- package/corpus/templates/slides/app/routes/access-request.approve.tsx +4 -1
- package/corpus/templates/slides/changelog/2026-10-06-fixed-returning-to-shared-decks-after-completing-an-account-.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-06-generated-decks-from-chatgpt-open-in-the-full-slides-editor.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-06-google-slides-exports-open-the-created-copy-directly.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-06-slides-applies-linked-design-systems-to-deck-style-reference.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-06-slides-stop-cleanly-when-generation-reaches-the-requested-sl.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-06-slides-uses-a-reference-deck-s-linked-system-only-when-avail.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-06-the-recent-tab-restores-immediately-from-the-last-known-libr.md +6 -0
- package/corpus/templates/slides/server/lib/chatgpt-directory-tools.ts +0 -2
- package/package.json +1 -1
package/corpus/README.md
CHANGED
|
@@ -70,12 +70,14 @@ export default defineAction({
|
|
|
70
70
|
kind: "sql",
|
|
71
71
|
archived,
|
|
72
72
|
hidden,
|
|
73
|
+
includeCatalogMetadata: true,
|
|
73
74
|
});
|
|
74
75
|
return rows.map((d) => ({
|
|
75
76
|
id: d.id,
|
|
76
77
|
name: d.name,
|
|
77
78
|
...(d.parentId ? { parentId: d.parentId } : {}),
|
|
78
79
|
...(d.folderId ? { folderId: d.folderId } : {}),
|
|
80
|
+
...(d.demoId ? { demo: true } : {}),
|
|
79
81
|
ownerEmail: d.ownerEmail,
|
|
80
82
|
orgId: d.orgId,
|
|
81
83
|
visibility: d.visibility,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineAction } from "@agent-native/core/action";
|
|
1
|
+
import { defineAction, type ActionRunContext } from "@agent-native/core/action";
|
|
2
2
|
import { getCredentialContext } from "@agent-native/core/server/request-context";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
@@ -14,18 +14,27 @@ export default defineAction({
|
|
|
14
14
|
schema: z.object({
|
|
15
15
|
source: z.enum(DASHBOARD_PANEL_SOURCES),
|
|
16
16
|
query: z.unknown(),
|
|
17
|
+
forceRefresh: z.boolean().optional(),
|
|
17
18
|
}),
|
|
18
19
|
http: { method: "POST" },
|
|
19
20
|
readOnly: true,
|
|
20
21
|
agentTool: false,
|
|
21
22
|
grounding: true,
|
|
22
|
-
run: async (args) => {
|
|
23
|
+
run: async (args, actionContext?: ActionRunContext) => {
|
|
23
24
|
const context = getCredentialContext();
|
|
24
25
|
if (!context) {
|
|
25
26
|
throw new Error("No authenticated context for query-dashboard-panel.");
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
const query = normalizeDashboardPanelQuery(args.source, args.query);
|
|
29
|
-
return resolveAnalyticsPanelSource(
|
|
30
|
+
return resolveAnalyticsPanelSource(
|
|
31
|
+
{
|
|
32
|
+
source: args.source,
|
|
33
|
+
query,
|
|
34
|
+
...(args.forceRefresh ? { forceRefresh: true } : {}),
|
|
35
|
+
...(actionContext?.signal ? { signal: actionContext.signal } : {}),
|
|
36
|
+
},
|
|
37
|
+
context,
|
|
38
|
+
);
|
|
30
39
|
},
|
|
31
40
|
});
|
|
@@ -1253,6 +1253,8 @@ interface SqlChartProps {
|
|
|
1253
1253
|
showLoadingWhenDisabled?: boolean;
|
|
1254
1254
|
timeRange?: number;
|
|
1255
1255
|
reportScreenshot?: boolean;
|
|
1256
|
+
refreshToken?: number;
|
|
1257
|
+
onRefreshRequested?: () => void;
|
|
1256
1258
|
onExportCsvChange?: (handler: (() => void) | null) => void;
|
|
1257
1259
|
onCopyTableChange?: (handler: (() => Promise<void>) | null) => void;
|
|
1258
1260
|
dashboardId?: string;
|
|
@@ -1267,6 +1269,8 @@ export function SqlChart({
|
|
|
1267
1269
|
showLoadingWhenDisabled = true,
|
|
1268
1270
|
timeRange,
|
|
1269
1271
|
reportScreenshot = false,
|
|
1272
|
+
refreshToken = 0,
|
|
1273
|
+
onRefreshRequested,
|
|
1270
1274
|
onExportCsvChange,
|
|
1271
1275
|
onCopyTableChange,
|
|
1272
1276
|
dashboardId,
|
|
@@ -1278,17 +1282,21 @@ export function SqlChart({
|
|
|
1278
1282
|
const isExtension = panel.chartType === "extension";
|
|
1279
1283
|
const shouldQuery = !isSection && !isExtension && loadData && !resultOverride;
|
|
1280
1284
|
const sql = serializePanelSql(resolvedSql ?? panel.sql);
|
|
1285
|
+
const [localRefreshToken, setLocalRefreshToken] = useState(0);
|
|
1281
1286
|
const {
|
|
1282
1287
|
data: queryResult,
|
|
1283
1288
|
isLoading: queryIsLoading,
|
|
1284
1289
|
isFetching: queryIsFetching,
|
|
1285
1290
|
error: queryError,
|
|
1286
|
-
refetch,
|
|
1287
1291
|
} = useSqlQuery(
|
|
1288
1292
|
["sql-chart", dashboardId || panel.id, sql, panel.source],
|
|
1289
1293
|
sql,
|
|
1290
1294
|
panel.source,
|
|
1291
|
-
{
|
|
1295
|
+
{
|
|
1296
|
+
enabled: shouldQuery,
|
|
1297
|
+
reportScreenshot,
|
|
1298
|
+
refreshToken: refreshToken + localRefreshToken,
|
|
1299
|
+
},
|
|
1292
1300
|
);
|
|
1293
1301
|
|
|
1294
1302
|
const result = resultOverride ?? queryResult;
|
|
@@ -1300,6 +1308,14 @@ export function SqlChart({
|
|
|
1300
1308
|
? (result?.error ??
|
|
1301
1309
|
(queryError ? formatSqlChartError(queryError) : undefined))
|
|
1302
1310
|
: undefined;
|
|
1311
|
+
const refreshError =
|
|
1312
|
+
shouldQuery && rawRows.length > 0 && queryError
|
|
1313
|
+
? formatSqlChartError(queryError)
|
|
1314
|
+
: undefined;
|
|
1315
|
+
const requestRefresh = () => {
|
|
1316
|
+
if (onRefreshRequested) onRefreshRequested();
|
|
1317
|
+
else setLocalRefreshToken((token) => token + 1);
|
|
1318
|
+
};
|
|
1303
1319
|
|
|
1304
1320
|
const { rows: queryRows, forcedYKeys } = useMemo(() => {
|
|
1305
1321
|
if (panel.config?.pivot && rawRows.length) {
|
|
@@ -1402,7 +1418,7 @@ export function SqlChart({
|
|
|
1402
1418
|
type="button"
|
|
1403
1419
|
variant="secondary"
|
|
1404
1420
|
size="sm"
|
|
1405
|
-
onClick={
|
|
1421
|
+
onClick={requestRefresh}
|
|
1406
1422
|
>
|
|
1407
1423
|
<IconRefresh className="mr-2 h-3.5 w-3.5" />
|
|
1408
1424
|
{t("sqlDashboard.refresh")}
|
|
@@ -1425,8 +1441,35 @@ export function SqlChart({
|
|
|
1425
1441
|
}
|
|
1426
1442
|
|
|
1427
1443
|
const missingConfigKeys = configuredKeysMissingFromRows(rows, panel);
|
|
1428
|
-
const withConfigWarning = (node: ReactNode) =>
|
|
1429
|
-
|
|
1444
|
+
const withConfigWarning = (node: ReactNode) => {
|
|
1445
|
+
if (refreshError) {
|
|
1446
|
+
return (
|
|
1447
|
+
<div className="flex h-full min-h-0 flex-col gap-2">
|
|
1448
|
+
<div
|
|
1449
|
+
className="flex items-center justify-between gap-3 rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2"
|
|
1450
|
+
role="alert"
|
|
1451
|
+
>
|
|
1452
|
+
<p className="min-w-0 break-words text-xs text-destructive">
|
|
1453
|
+
{refreshError}
|
|
1454
|
+
</p>
|
|
1455
|
+
<Button
|
|
1456
|
+
type="button"
|
|
1457
|
+
variant="secondary"
|
|
1458
|
+
size="sm"
|
|
1459
|
+
onClick={requestRefresh}
|
|
1460
|
+
>
|
|
1461
|
+
<IconRefresh className="mr-2 h-3.5 w-3.5" />
|
|
1462
|
+
{t("sqlDashboard.refresh")}
|
|
1463
|
+
</Button>
|
|
1464
|
+
</div>
|
|
1465
|
+
{missingConfigKeys.length > 0 && (
|
|
1466
|
+
<ConfigWarning keys={missingConfigKeys} />
|
|
1467
|
+
)}
|
|
1468
|
+
<div className="min-h-0 flex-1">{node}</div>
|
|
1469
|
+
</div>
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
return missingConfigKeys.length > 0 ? (
|
|
1430
1473
|
<div className="space-y-2">
|
|
1431
1474
|
<ConfigWarning keys={missingConfigKeys} />
|
|
1432
1475
|
{node}
|
|
@@ -1434,6 +1477,7 @@ export function SqlChart({
|
|
|
1434
1477
|
) : (
|
|
1435
1478
|
node
|
|
1436
1479
|
);
|
|
1480
|
+
};
|
|
1437
1481
|
|
|
1438
1482
|
if (chartType === "metric") {
|
|
1439
1483
|
return withConfigWarning(<MetricRenderer rows={rows} panel={panel} />);
|
|
@@ -71,6 +71,9 @@ type SidebarDashboard = {
|
|
|
71
71
|
resourceId?: string;
|
|
72
72
|
visibility?: Visibility;
|
|
73
73
|
ownerEmail?: string | null;
|
|
74
|
+
demo?: boolean;
|
|
75
|
+
/** Owner label, set only when another dashboard shares this title. */
|
|
76
|
+
ownerHint?: string;
|
|
74
77
|
parentId?: string;
|
|
75
78
|
};
|
|
76
79
|
|
|
@@ -486,6 +489,7 @@ function SortableRow({
|
|
|
486
489
|
id,
|
|
487
490
|
favoriteKey,
|
|
488
491
|
name,
|
|
492
|
+
hint,
|
|
489
493
|
href,
|
|
490
494
|
isActive,
|
|
491
495
|
favoriteIds,
|
|
@@ -504,6 +508,7 @@ function SortableRow({
|
|
|
504
508
|
id: string;
|
|
505
509
|
favoriteKey: string;
|
|
506
510
|
name: string;
|
|
511
|
+
hint?: string;
|
|
507
512
|
href: string;
|
|
508
513
|
isActive: boolean;
|
|
509
514
|
favoriteIds: Set<string>;
|
|
@@ -725,10 +730,17 @@ function SortableRow({
|
|
|
725
730
|
onTouchStart={onPrefetch}
|
|
726
731
|
className="min-w-0 flex-1 px-2 py-1.5 pe-12 text-xs transition-[padding] md:pe-2 md:group-hover/item:pe-12 md:group-focus-within/item:pe-12"
|
|
727
732
|
>
|
|
728
|
-
<span className="block truncate">
|
|
733
|
+
<span className="block truncate">
|
|
734
|
+
{name}
|
|
735
|
+
{hint ? (
|
|
736
|
+
<span className="text-muted-foreground"> · {hint}</span>
|
|
737
|
+
) : null}
|
|
738
|
+
</span>
|
|
729
739
|
</Link>
|
|
730
740
|
</TooltipTrigger>
|
|
731
|
-
<TooltipContent side="right">
|
|
741
|
+
<TooltipContent side="right">
|
|
742
|
+
{hint ? `${name} · ${hint}` : name}
|
|
743
|
+
</TooltipContent>
|
|
732
744
|
</Tooltip>
|
|
733
745
|
)}
|
|
734
746
|
<div className="pointer-events-none absolute end-1 top-1/2 flex -translate-y-1/2 items-center gap-0.5 opacity-100 transition-opacity md:opacity-0 md:group-hover/item:opacity-100 md:group-focus-within/item:opacity-100">
|
|
@@ -981,6 +993,7 @@ function SortableDashboardItem({
|
|
|
981
993
|
id={d.id}
|
|
982
994
|
favoriteKey={favoriteKey}
|
|
983
995
|
name={d.name}
|
|
996
|
+
hint={d.ownerHint}
|
|
984
997
|
href={href}
|
|
985
998
|
isActive={isActive}
|
|
986
999
|
favoriteIds={favoriteIds}
|
|
@@ -1182,6 +1195,7 @@ type SqlDashboardListItem = {
|
|
|
1182
1195
|
name: string;
|
|
1183
1196
|
visibility?: Visibility;
|
|
1184
1197
|
ownerEmail?: string | null;
|
|
1198
|
+
demo?: boolean;
|
|
1185
1199
|
parentId?: string;
|
|
1186
1200
|
};
|
|
1187
1201
|
|
|
@@ -1211,6 +1225,7 @@ async function fetchSqlDashboards(
|
|
|
1211
1225
|
? d.name
|
|
1212
1226
|
: t("sidebar.untitledDashboard"),
|
|
1213
1227
|
visibility: d.visibility as Visibility,
|
|
1228
|
+
...(d.demo === true ? { demo: true } : {}),
|
|
1214
1229
|
...(ownerEmail ? { ownerEmail } : {}),
|
|
1215
1230
|
parentId:
|
|
1216
1231
|
typeof d.parentId === "string" && d.parentId.trim().length > 0
|
|
@@ -1225,6 +1240,7 @@ async function fetchSidebarAnalyses(t: (key: string) => string): Promise<
|
|
|
1225
1240
|
id: string;
|
|
1226
1241
|
name: string;
|
|
1227
1242
|
visibility: Visibility;
|
|
1243
|
+
ownerEmail?: string;
|
|
1228
1244
|
hiddenAt: string | null;
|
|
1229
1245
|
}[]
|
|
1230
1246
|
> {
|
|
@@ -1241,6 +1257,9 @@ async function fetchSidebarAnalyses(t: (key: string) => string): Promise<
|
|
|
1241
1257
|
a.visibility === "org" || a.visibility === "public"
|
|
1242
1258
|
? a.visibility
|
|
1243
1259
|
: ("private" as Visibility),
|
|
1260
|
+
...(typeof a.ownerEmail === "string" && a.ownerEmail
|
|
1261
|
+
? { ownerEmail: a.ownerEmail }
|
|
1262
|
+
: {}),
|
|
1244
1263
|
hiddenAt: typeof a.hiddenAt === "string" ? a.hiddenAt : null,
|
|
1245
1264
|
}));
|
|
1246
1265
|
}
|
|
@@ -1801,6 +1820,7 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) {
|
|
|
1801
1820
|
source: "sql",
|
|
1802
1821
|
visibility: d.visibility,
|
|
1803
1822
|
ownerEmail: d.ownerEmail,
|
|
1823
|
+
demo: d.demo,
|
|
1804
1824
|
parentId: d.parentId,
|
|
1805
1825
|
}));
|
|
1806
1826
|
const analysisItems: SidebarDashboard[] = analysesList.map((a) => ({
|
|
@@ -1809,6 +1829,7 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) {
|
|
|
1809
1829
|
name: a.name,
|
|
1810
1830
|
source: "analysis",
|
|
1811
1831
|
visibility: a.visibility,
|
|
1832
|
+
...(a.ownerEmail ? { ownerEmail: a.ownerEmail } : {}),
|
|
1812
1833
|
}));
|
|
1813
1834
|
const all = [...staticItems, ...sqlItems, ...analysisItems];
|
|
1814
1835
|
if (dashboardSortMode === "alphabetical") {
|
|
@@ -1851,13 +1872,22 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) {
|
|
|
1851
1872
|
popularity,
|
|
1852
1873
|
]);
|
|
1853
1874
|
|
|
1854
|
-
const filteredDashboards = useMemo(
|
|
1855
|
-
()
|
|
1856
|
-
|
|
1875
|
+
const filteredDashboards = useMemo(() => {
|
|
1876
|
+
const titleCounts = new Map<string, number>();
|
|
1877
|
+
for (const d of visibleDashboards) {
|
|
1878
|
+
const key = d.name.trim().toLowerCase();
|
|
1879
|
+
titleCounts.set(key, (titleCounts.get(key) ?? 0) + 1);
|
|
1880
|
+
}
|
|
1881
|
+
return visibleDashboards
|
|
1882
|
+
.filter((dashboard) =>
|
|
1857
1883
|
matchesVisibilityFilter(dashboard, dashFilter, auth?.email),
|
|
1858
|
-
)
|
|
1859
|
-
|
|
1860
|
-
|
|
1884
|
+
)
|
|
1885
|
+
.map((d) =>
|
|
1886
|
+
d.ownerEmail && (titleCounts.get(d.name.trim().toLowerCase()) ?? 0) > 1
|
|
1887
|
+
? { ...d, ownerHint: d.ownerEmail.split("@")[0] }
|
|
1888
|
+
: d,
|
|
1889
|
+
);
|
|
1890
|
+
}, [auth?.email, visibleDashboards, dashFilter]);
|
|
1861
1891
|
|
|
1862
1892
|
const dashboardChildren = useMemo<Map<string, SidebarDashboard[]>>(() => {
|
|
1863
1893
|
const byId = new Map(filteredDashboards.map((d) => [d.id, d]));
|
|
@@ -254,7 +254,7 @@ export default {
|
|
|
254
254
|
visibilityPrivateOnly: "Mine",
|
|
255
255
|
visibilitySharedOnly: "Shared",
|
|
256
256
|
visibilityAllDescription: "Show all items",
|
|
257
|
-
visibilityPrivateOnlyDescription: "Show items
|
|
257
|
+
visibilityPrivateOnlyDescription: "Show items you created",
|
|
258
258
|
visibilitySharedOnlyDescription: "Show org-shared and public items",
|
|
259
259
|
hiddenAnalyses: "Hidden analyses",
|
|
260
260
|
shareWithOrg: "Share with org",
|
|
@@ -4,6 +4,8 @@ export type DashboardVisibilityFilter = "all" | "private" | "shared";
|
|
|
4
4
|
export type DashboardVisibilityItem = {
|
|
5
5
|
visibility?: DashboardVisibility;
|
|
6
6
|
ownerEmail?: string | null;
|
|
7
|
+
/** Sample content installed for the user; never theirs, never someone else's. */
|
|
8
|
+
demo?: boolean;
|
|
7
9
|
};
|
|
8
10
|
|
|
9
11
|
function normalizeEmail(email: string | null | undefined): string | null {
|
|
@@ -28,13 +30,26 @@ export function isDashboardMine(
|
|
|
28
30
|
return true;
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
// Sidebar "Mine" means "I own it", whether or not I have shared it. The
|
|
34
|
+
// Overview's personal/shared scope keeps `isDashboardMine`: it picks folders.
|
|
35
|
+
function isOwnedByViewer(
|
|
36
|
+
item: DashboardVisibilityItem,
|
|
37
|
+
currentUserEmail?: string | null,
|
|
38
|
+
): boolean {
|
|
39
|
+
if (!("ownerEmail" in item)) return item.visibility === "private";
|
|
40
|
+
const ownerEmail = normalizeEmail(item.ownerEmail);
|
|
41
|
+
const viewerEmail = normalizeEmail(currentUserEmail);
|
|
42
|
+
return Boolean(ownerEmail && viewerEmail && ownerEmail === viewerEmail);
|
|
43
|
+
}
|
|
44
|
+
|
|
31
45
|
export function matchesDashboardVisibilityFilter(
|
|
32
46
|
item: DashboardVisibilityItem,
|
|
33
47
|
filter: DashboardVisibilityFilter,
|
|
34
48
|
currentUserEmail?: string | null,
|
|
35
49
|
): boolean {
|
|
36
50
|
if (filter === "all") return true;
|
|
51
|
+
if (item.demo) return false;
|
|
37
52
|
return filter === "private"
|
|
38
|
-
?
|
|
39
|
-
: !
|
|
53
|
+
? isOwnedByViewer(item, currentUserEmail)
|
|
54
|
+
: !isOwnedByViewer(item, currentUserEmail);
|
|
40
55
|
}
|
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
MAX_CONCURRENT_FIRST_PARTY_SQL_QUERIES,
|
|
5
5
|
MAX_CONCURRENT_SQL_QUERIES,
|
|
6
6
|
} from "@shared/sql-query-limits";
|
|
7
|
-
import { useQuery } from "@tanstack/react-query";
|
|
7
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
8
|
+
import { useEffect, useRef } from "react";
|
|
8
9
|
|
|
9
10
|
import type { DataSourceType } from "@/pages/adhoc/sql-dashboard/types";
|
|
10
11
|
|
|
@@ -138,7 +139,7 @@ export async function executeSqlQuery(
|
|
|
138
139
|
sql: string,
|
|
139
140
|
source: DataSourceType,
|
|
140
141
|
signal?: AbortSignal,
|
|
141
|
-
options?: { reportScreenshot?: boolean },
|
|
142
|
+
options?: { reportScreenshot?: boolean; forceRefresh?: boolean },
|
|
142
143
|
): Promise<SqlQueryResult> {
|
|
143
144
|
const deadline = createDeadlineSignal(
|
|
144
145
|
signal,
|
|
@@ -150,7 +151,13 @@ export async function executeSqlQuery(
|
|
|
150
151
|
release = await acquireSqlQuerySlot(source, deadline.signal);
|
|
151
152
|
data = await callAction<DashboardPanelQueryResponse>(
|
|
152
153
|
"query-dashboard-panel",
|
|
153
|
-
{
|
|
154
|
+
{
|
|
155
|
+
query: sql,
|
|
156
|
+
source,
|
|
157
|
+
...(options?.forceRefresh && source === "bigquery"
|
|
158
|
+
? { forceRefresh: true }
|
|
159
|
+
: {}),
|
|
160
|
+
},
|
|
154
161
|
{
|
|
155
162
|
signal: deadline.signal,
|
|
156
163
|
timeoutMs: DASHBOARD_REPORT_ACTION_TIMEOUT_MS,
|
|
@@ -191,16 +198,36 @@ export function useSqlQuery(
|
|
|
191
198
|
refetchOnWindowFocus?: boolean | "always";
|
|
192
199
|
retry?: boolean | number;
|
|
193
200
|
reportScreenshot?: boolean;
|
|
201
|
+
refreshToken?: number;
|
|
194
202
|
staleTime?: number;
|
|
195
203
|
},
|
|
196
204
|
) {
|
|
197
|
-
|
|
205
|
+
const successfulRefreshToken = useRef(0);
|
|
206
|
+
const refreshToken = options?.refreshToken ?? 0;
|
|
207
|
+
const enabled = options?.enabled ?? true;
|
|
208
|
+
const latestRefreshToken = useRef(refreshToken);
|
|
209
|
+
const latestStartedRefreshToken = useRef(refreshToken);
|
|
210
|
+
latestRefreshToken.current = refreshToken;
|
|
211
|
+
const previousRefreshToken = useRef(refreshToken);
|
|
212
|
+
const previousEnabled = useRef(enabled);
|
|
213
|
+
const queryClient = useQueryClient();
|
|
214
|
+
const query = useQuery<SqlQueryResult>({
|
|
198
215
|
queryKey,
|
|
199
|
-
queryFn: ({ signal }) =>
|
|
200
|
-
|
|
216
|
+
queryFn: async ({ signal }) => {
|
|
217
|
+
const currentRefreshToken = latestRefreshToken.current;
|
|
218
|
+
latestStartedRefreshToken.current = currentRefreshToken;
|
|
219
|
+
const forceRefresh = currentRefreshToken > successfulRefreshToken.current;
|
|
220
|
+
const result = await executeSqlQuery(sql, source, signal, {
|
|
201
221
|
reportScreenshot: options?.reportScreenshot,
|
|
202
|
-
|
|
203
|
-
|
|
222
|
+
forceRefresh,
|
|
223
|
+
});
|
|
224
|
+
successfulRefreshToken.current = Math.max(
|
|
225
|
+
successfulRefreshToken.current,
|
|
226
|
+
currentRefreshToken,
|
|
227
|
+
);
|
|
228
|
+
return result;
|
|
229
|
+
},
|
|
230
|
+
enabled,
|
|
204
231
|
refetchInterval: options?.refetchInterval,
|
|
205
232
|
refetchOnMount: options?.refetchOnMount ?? false,
|
|
206
233
|
refetchOnReconnect: options?.refetchOnReconnect ?? false,
|
|
@@ -208,4 +235,29 @@ export function useSqlQuery(
|
|
|
208
235
|
retry: options?.retry ?? false,
|
|
209
236
|
staleTime: options?.staleTime ?? 5 * 60 * 1000,
|
|
210
237
|
});
|
|
238
|
+
const refetch = query.refetch;
|
|
239
|
+
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
const wasEnabled = previousEnabled.current;
|
|
242
|
+
previousEnabled.current = enabled;
|
|
243
|
+
const refreshTokenChanged = refreshToken > previousRefreshToken.current;
|
|
244
|
+
previousRefreshToken.current = refreshToken;
|
|
245
|
+
if (!enabled) return;
|
|
246
|
+
const enablingPendingRefresh =
|
|
247
|
+
!wasEnabled && refreshToken > successfulRefreshToken.current;
|
|
248
|
+
if (
|
|
249
|
+
(!refreshTokenChanged && !enablingPendingRefresh) ||
|
|
250
|
+
refreshToken <= successfulRefreshToken.current
|
|
251
|
+
) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (query.isFetching && latestStartedRefreshToken.current >= refreshToken) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
void queryClient
|
|
258
|
+
.cancelQueries({ queryKey, exact: true })
|
|
259
|
+
.then(() => refetch());
|
|
260
|
+
}, [enabled, query.isFetching, queryClient, queryKey, refetch, refreshToken]);
|
|
261
|
+
|
|
262
|
+
return query;
|
|
211
263
|
}
|
|
@@ -16,8 +16,7 @@ import {
|
|
|
16
16
|
IconBrandGoogle,
|
|
17
17
|
IconArrowUpRight,
|
|
18
18
|
} from "@tabler/icons-react";
|
|
19
|
-
import {
|
|
20
|
-
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
19
|
+
import { memo, useCallback, useEffect, useRef, useState } from "react";
|
|
21
20
|
import { toast } from "sonner";
|
|
22
21
|
|
|
23
22
|
import { ChartFillHeight, SqlChart } from "@/components/dashboard/SqlChart";
|
|
@@ -62,7 +61,6 @@ import type { SelectDashboardPanelOptions } from "@/hooks/use-dashboard-chat-con
|
|
|
62
61
|
import { buildCustomBlockPromotionRequest } from "@/lib/custom-block-promotion";
|
|
63
62
|
import { cn } from "@/lib/utils";
|
|
64
63
|
|
|
65
|
-
import { serializePanelSql } from "./panel-sql";
|
|
66
64
|
import { timeRangeDays } from "./pivot";
|
|
67
65
|
import type { DashboardFilter, SqlPanel } from "./types";
|
|
68
66
|
import { ViewSqlPopover } from "./ViewSqlPopover";
|
|
@@ -152,7 +150,6 @@ export function SqlChartCard({
|
|
|
152
150
|
}: SqlChartCardProps) {
|
|
153
151
|
const t = useT();
|
|
154
152
|
const timeRange = timeRangeDays(filters?.timeRange);
|
|
155
|
-
const queryClient = useQueryClient();
|
|
156
153
|
const exportToGoogleSheets = useActionMutation(
|
|
157
154
|
"export-dashboard-panel-to-google-sheet",
|
|
158
155
|
);
|
|
@@ -172,17 +169,8 @@ export function SqlChartCard({
|
|
|
172
169
|
panel.chartType === "section" ||
|
|
173
170
|
panel.chartType === "extension",
|
|
174
171
|
);
|
|
172
|
+
const [refreshToken, setRefreshToken] = useState(0);
|
|
175
173
|
const cardRef = useRef<HTMLDivElement | null>(null);
|
|
176
|
-
const chartQueryKey = useMemo(
|
|
177
|
-
() =>
|
|
178
|
-
[
|
|
179
|
-
"sql-chart",
|
|
180
|
-
dashboardId || panel.id,
|
|
181
|
-
serializePanelSql(resolvedSql ?? panel.sql),
|
|
182
|
-
panel.source,
|
|
183
|
-
] as const,
|
|
184
|
-
[dashboardId, panel.id, panel.source, panel.sql, resolvedSql],
|
|
185
|
-
);
|
|
186
174
|
const setCardNodeRef = useCallback((node: HTMLDivElement | null) => {
|
|
187
175
|
cardRef.current = node;
|
|
188
176
|
}, []);
|
|
@@ -210,10 +198,8 @@ export function SqlChartCard({
|
|
|
210
198
|
|
|
211
199
|
const handleRefresh = useCallback(() => {
|
|
212
200
|
setShouldLoadData(true);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
});
|
|
216
|
-
}, [chartQueryKey, queryClient]);
|
|
201
|
+
setRefreshToken((token) => token + 1);
|
|
202
|
+
}, []);
|
|
217
203
|
|
|
218
204
|
const handleExportToGoogleSheets = useCallback(async () => {
|
|
219
205
|
if (!dashboardId || panel.chartType !== "table") return;
|
|
@@ -784,6 +770,8 @@ export function SqlChartCard({
|
|
|
784
770
|
loadData={shouldLoadData}
|
|
785
771
|
timeRange={timeRange}
|
|
786
772
|
reportScreenshot={reportScreenshot}
|
|
773
|
+
refreshToken={refreshToken}
|
|
774
|
+
onRefreshRequested={handleRefresh}
|
|
787
775
|
dashboardId={dashboardId}
|
|
788
776
|
onExportCsvChange={handleExportCsvChange}
|
|
789
777
|
onCopyTableChange={handleCopyTableChange}
|
|
@@ -805,6 +793,8 @@ export function SqlChartCard({
|
|
|
805
793
|
loadData
|
|
806
794
|
timeRange={timeRange}
|
|
807
795
|
reportScreenshot={reportScreenshot}
|
|
796
|
+
refreshToken={refreshToken}
|
|
797
|
+
onRefreshRequested={handleRefresh}
|
|
808
798
|
dashboardId={dashboardId}
|
|
809
799
|
extensionContext={extensionContext}
|
|
810
800
|
/>
|
|
@@ -182,6 +182,11 @@ export const bigqueryCache = table("bigquery_cache", {
|
|
|
182
182
|
bytesProcessed: integer("bytes_processed").notNull().default(0),
|
|
183
183
|
createdAt: text("created_at").notNull(),
|
|
184
184
|
expiresAt: text("expires_at").notNull(),
|
|
185
|
+
generation: integer("generation").notNull().default(0),
|
|
186
|
+
fenceToken: text("fence_token"),
|
|
187
|
+
refreshInProgress: boolean("refresh_in_progress").notNull().default(false),
|
|
188
|
+
refreshForced: boolean("refresh_forced").notNull().default(false),
|
|
189
|
+
refreshStartedAt: text("refresh_started_at"),
|
|
185
190
|
});
|
|
186
191
|
|
|
187
192
|
export const firstPartyAnalyticsCache = table("first_party_analytics_cache", {
|