@agent-native/core 0.168.13 → 0.169.1
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/bigquery-table-info.ts +3 -2
- package/corpus/templates/analytics/actions/search-bigquery-schema.ts +130 -4
- package/corpus/templates/analytics/agent-native.config.ts +1 -0
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +36 -23
- package/corpus/templates/analytics/app/lib/dashboard-visibility.ts +41 -0
- package/corpus/templates/analytics/app/pages/DashboardOverview.tsx +34 -9
- package/corpus/templates/analytics/changelog/2026-08-21-dashboard-filters-keep-other-users-dashboards-out-of-mine.md +6 -0
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +14 -3
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +33 -3
- package/corpus/templates/brain/server/lib/brain.ts +27 -2
- package/corpus/templates/clips/actions/stop-meeting-recording.ts +87 -6
- package/corpus/templates/clips/app/components/library/search-bar.tsx +1 -1
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +3 -4
- package/corpus/templates/design/app/components/design/EditPanel.tsx +15 -47
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +3 -3
- package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +0 -7
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +2 -4
- package/corpus/templates/design/app/i18n-data.ts +10 -10
- package/corpus/templates/design/app/pages/DesignEditor.tsx +22 -22
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +39 -8
- package/corpus/templates/design/app/pages/DesignSystems.tsx +1 -1
- package/corpus/templates/design/app/pages/Index.tsx +1 -1
- package/corpus/templates/design/app/pages/design-editor/preview-navigation.ts +25 -0
- package/corpus/templates/mail/app/hooks/use-emails.ts +13 -3
- package/dist/agent/engine/credential-errors.d.ts +1 -1
- package/dist/agent/engine/credential-errors.js +2 -2
- package/dist/agent/engine/first-event-timeout.d.ts +8 -0
- package/dist/agent/engine/first-event-timeout.js +8 -0
- package/dist/agent/production-agent.d.ts +0 -30
- package/dist/agent/production-agent.js +17 -38
- package/dist/agent/run-loop-with-resume.d.ts +38 -25
- package/dist/agent/run-loop-with-resume.js +140 -55
- package/dist/agent/run-manager.d.ts +83 -68
- package/dist/agent/run-manager.js +280 -94
- package/dist/agent/run-store.d.ts +31 -0
- package/dist/agent/run-store.js +42 -12
- package/dist/app-config/agent.d.ts +2 -0
- package/dist/app-config/agent.js +33 -0
- package/dist/app-config/run-lifecycle-invariants.d.ts +248 -0
- package/dist/app-config/run-lifecycle-invariants.js +342 -0
- package/dist/app-config/schema.d.ts +2 -0
- package/dist/app-config/store.js +9 -1
- package/dist/client/AssistantChat.js +47 -15
- package/dist/client/ConnectBuilderCard.d.ts +3 -1
- package/dist/client/ConnectBuilderCard.js +6 -3
- package/dist/client/agent-chat-adapter.d.ts +0 -2
- package/dist/client/agent-chat-adapter.js +7 -23
- package/dist/client/chat/message-components.d.ts +7 -0
- package/dist/client/chat/message-components.js +1 -1
- package/dist/client/chat/run-recovery.js +5 -2
- package/dist/client/chat/tool-call-display.js +9 -3
- package/dist/client/error-format.js +12 -0
- package/dist/client/navigation/index.d.ts +1 -0
- package/dist/client/navigation/index.js +1 -0
- package/dist/client/route-state.js +6 -0
- package/dist/client/workspace-app-navigation.d.ts +10 -0
- package/dist/client/workspace-app-navigation.js +38 -0
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.d.ts +13 -0
- package/dist/deploy/build.js +51 -0
- package/dist/jobs/background-automation-runner.d.ts +25 -0
- package/dist/jobs/background-automation-runner.js +104 -21
- package/dist/jobs/run-history.d.ts +7 -1
- package/dist/jobs/run-history.js +57 -14
- package/dist/localization/core-messages/ar-SA.js +1 -1
- package/dist/localization/core-messages/de-DE.js +1 -1
- package/dist/localization/core-messages/en-US.d.ts +1 -1
- package/dist/localization/core-messages/en-US.js +1 -1
- package/dist/localization/core-messages/es-ES.js +1 -1
- package/dist/localization/core-messages/fr-FR.js +1 -1
- package/dist/localization/core-messages/hi-IN.js +1 -1
- package/dist/localization/core-messages/ja-JP.js +1 -1
- package/dist/localization/core-messages/ko-KR.js +1 -1
- package/dist/localization/core-messages/pt-BR.js +1 -1
- package/dist/localization/core-messages/zh-CN.js +1 -1
- package/dist/localization/core-messages/zh-TW.js +1 -1
- package/dist/localization/core-messages.d.ts +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/observability/traces.d.ts +13 -0
- package/dist/observability/traces.js +369 -317
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
- package/dist/provider-api/actions/provider-api.d.ts +13 -13
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/db/check-scoping.js +9 -6
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/agent-chat-plugin.js +2 -4
- package/dist/server/builder-browser.d.ts +4 -0
- package/dist/server/builder-browser.js +23 -1
- package/dist/server/core-routes-plugin.js +13 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +3 -3
- /package/corpus/templates/dispatch/app/routes/{apps.$appId.tsx → apps.$appId.$.tsx} +0 -0
package/corpus/README.md
CHANGED
|
@@ -9,12 +9,12 @@ Use these sources of truth instead:
|
|
|
9
9
|
|
|
10
10
|
- \`list-data-dictionary\` for approved metric, table, column, join, and caveat definitions configured by this organization.
|
|
11
11
|
- \`data-source-status --key bigquery\` to confirm whether BigQuery credentials and a project are configured.
|
|
12
|
-
- \`search-bigquery-schema\` to
|
|
12
|
+
- \`search-bigquery-schema\` to search the configured project by dataset, table, or column term, or inspect exact table columns before writing SQL.
|
|
13
13
|
- The configured warehouse's \`INFORMATION_SCHEMA.COLUMNS\` tables when you need to discover actual datasets, tables, and columns.
|
|
14
14
|
- \`@app_events\` as an optional placeholder for a default application events table in query-metrics examples. It defaults to \`<BIGQUERY_PROJECT_ID>.analytics.events_partitioned\` and can be overridden with \`ANALYTICS_BIGQUERY_EVENTS_TABLE\`; it is not the BigQuery connection status or a table catalog.
|
|
15
15
|
- \`@project\` as the configured BigQuery project placeholder for fully-qualified table references.
|
|
16
16
|
|
|
17
|
-
If no data-dictionary entry exists
|
|
17
|
+
If no data-dictionary entry exists, search the configured project before asking about business meaning. Ask the user for a metric definition only after the configured schema and query actions prove that the source or metric is unavailable. Never ask the user to provide internal dataset, table, or column names that the schema action can discover, and never invent table names, column names, joins, or numbers.
|
|
18
18
|
|
|
19
19
|
Preferred schema discovery:
|
|
20
20
|
|
|
@@ -22,6 +22,7 @@ Preferred schema discovery:
|
|
|
22
22
|
pnpm action search-bigquery-schema
|
|
23
23
|
pnpm action search-bigquery-schema --dataset=<dataset>
|
|
24
24
|
pnpm action search-bigquery-schema --table=<dataset.table>
|
|
25
|
+
pnpm action search-bigquery-schema --search=<metric-or-column-term>
|
|
25
26
|
\`\`\`
|
|
26
27
|
|
|
27
28
|
Example schema discovery query:
|
|
@@ -35,6 +35,15 @@ interface TableListResponse {
|
|
|
35
35
|
}>;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
interface BigQueryTableSummary {
|
|
39
|
+
projectId?: string;
|
|
40
|
+
datasetId?: string;
|
|
41
|
+
tableId?: string;
|
|
42
|
+
type?: string;
|
|
43
|
+
friendlyName?: string;
|
|
44
|
+
labels?: Record<string, string>;
|
|
45
|
+
}
|
|
46
|
+
|
|
38
47
|
interface TableMetadata {
|
|
39
48
|
tableReference?: {
|
|
40
49
|
projectId?: string;
|
|
@@ -54,6 +63,9 @@ interface TableMetadata {
|
|
|
54
63
|
|
|
55
64
|
const PROJECT_RE = /^[A-Za-z][A-Za-z0-9-]{4,61}[A-Za-z0-9]$/;
|
|
56
65
|
const ID_RE = /^[A-Za-z0-9_]+$/;
|
|
66
|
+
const GLOBAL_SEARCH_DATASET_LIMIT = 100;
|
|
67
|
+
const GLOBAL_SEARCH_TABLE_LIMIT = 250;
|
|
68
|
+
const GLOBAL_SEARCH_METADATA_BATCH_SIZE = 20;
|
|
57
69
|
|
|
58
70
|
function assertIdentifier(
|
|
59
71
|
label: string,
|
|
@@ -183,7 +195,12 @@ function compactTable(meta: TableMetadata, includeColumns: boolean) {
|
|
|
183
195
|
}
|
|
184
196
|
|
|
185
197
|
function matchesSearch(meta: TableMetadata, search: string): boolean {
|
|
186
|
-
const
|
|
198
|
+
const terms = search
|
|
199
|
+
.toLowerCase()
|
|
200
|
+
.replace(/[._-]+/g, " ")
|
|
201
|
+
.split(/\s+/)
|
|
202
|
+
.filter(Boolean);
|
|
203
|
+
if (!terms.length) return true;
|
|
187
204
|
const ref = meta.tableReference ?? {};
|
|
188
205
|
const haystack = [
|
|
189
206
|
ref.projectId,
|
|
@@ -199,8 +216,9 @@ function matchesSearch(meta: TableMetadata, search: string): boolean {
|
|
|
199
216
|
]
|
|
200
217
|
.filter(Boolean)
|
|
201
218
|
.join(" ")
|
|
202
|
-
.toLowerCase()
|
|
203
|
-
|
|
219
|
+
.toLowerCase()
|
|
220
|
+
.replace(/[._-]+/g, " ");
|
|
221
|
+
return terms.every((term) => haystack.includes(term));
|
|
204
222
|
}
|
|
205
223
|
|
|
206
224
|
async function listDatasets(projectId: string, limit: number, search: string) {
|
|
@@ -245,6 +263,111 @@ async function listTables(projectId: string, datasetId: string, limit: number) {
|
|
|
245
263
|
}));
|
|
246
264
|
}
|
|
247
265
|
|
|
266
|
+
async function searchAcrossDatasets(
|
|
267
|
+
projectId: string,
|
|
268
|
+
search: string,
|
|
269
|
+
limit: number,
|
|
270
|
+
) {
|
|
271
|
+
const datasets = await listDatasets(
|
|
272
|
+
projectId,
|
|
273
|
+
GLOBAL_SEARCH_DATASET_LIMIT + 1,
|
|
274
|
+
"",
|
|
275
|
+
);
|
|
276
|
+
const scannableDatasets = datasets.slice(0, GLOBAL_SEARCH_DATASET_LIMIT);
|
|
277
|
+
const tables: BigQueryTableSummary[] = [];
|
|
278
|
+
const datasetCount = scannableDatasets.filter(
|
|
279
|
+
(dataset) => typeof dataset.datasetId === "string" && dataset.datasetId,
|
|
280
|
+
).length;
|
|
281
|
+
let datasetsScanned = 0;
|
|
282
|
+
let truncated = datasets.length > GLOBAL_SEARCH_DATASET_LIMIT;
|
|
283
|
+
|
|
284
|
+
for (const dataset of scannableDatasets) {
|
|
285
|
+
const datasetId = dataset.datasetId;
|
|
286
|
+
if (!datasetId) continue;
|
|
287
|
+
if (tables.length >= GLOBAL_SEARCH_TABLE_LIMIT) {
|
|
288
|
+
truncated = true;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
datasetsScanned += 1;
|
|
293
|
+
const remaining = GLOBAL_SEARCH_TABLE_LIMIT - tables.length;
|
|
294
|
+
const listed = await listTables(
|
|
295
|
+
projectId,
|
|
296
|
+
datasetId,
|
|
297
|
+
Math.min(remaining, GLOBAL_SEARCH_TABLE_LIMIT),
|
|
298
|
+
);
|
|
299
|
+
tables.push(...listed);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (datasetsScanned < datasetCount) truncated = true;
|
|
303
|
+
|
|
304
|
+
// Inspect table metadata in bounded batches so a search can match columns,
|
|
305
|
+
// not only table names, without issuing an unbounded burst of requests.
|
|
306
|
+
const matches: ReturnType<typeof compactTable>[] = [];
|
|
307
|
+
const errors: Array<{
|
|
308
|
+
projectId?: string;
|
|
309
|
+
datasetId?: string;
|
|
310
|
+
tableId?: string;
|
|
311
|
+
error: string;
|
|
312
|
+
}> = [];
|
|
313
|
+
|
|
314
|
+
for (
|
|
315
|
+
let offset = 0;
|
|
316
|
+
offset < tables.length;
|
|
317
|
+
offset += GLOBAL_SEARCH_METADATA_BATCH_SIZE
|
|
318
|
+
) {
|
|
319
|
+
const batch = tables.slice(
|
|
320
|
+
offset,
|
|
321
|
+
offset + GLOBAL_SEARCH_METADATA_BATCH_SIZE,
|
|
322
|
+
);
|
|
323
|
+
const settled = await Promise.allSettled(
|
|
324
|
+
batch.map(async (table) => {
|
|
325
|
+
if (!table.datasetId || !table.tableId) return null;
|
|
326
|
+
const metadata = await getTableMetadata(
|
|
327
|
+
projectId,
|
|
328
|
+
table.datasetId,
|
|
329
|
+
table.tableId,
|
|
330
|
+
);
|
|
331
|
+
return matchesSearch(metadata, search)
|
|
332
|
+
? compactTable(metadata, true)
|
|
333
|
+
: null;
|
|
334
|
+
}),
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
settled.forEach((result, index) => {
|
|
338
|
+
const table = batch[index];
|
|
339
|
+
if (result.status === "fulfilled") {
|
|
340
|
+
if (result.value) matches.push(result.value);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
errors.push({
|
|
344
|
+
projectId: table?.projectId ?? projectId,
|
|
345
|
+
datasetId: table?.datasetId,
|
|
346
|
+
tableId: table?.tableId,
|
|
347
|
+
error:
|
|
348
|
+
result.reason instanceof Error
|
|
349
|
+
? result.reason.message
|
|
350
|
+
: String(result.reason),
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
mode: "table-search",
|
|
357
|
+
projectId,
|
|
358
|
+
search,
|
|
359
|
+
datasetsScanned,
|
|
360
|
+
tablesScanned: tables.length,
|
|
361
|
+
truncated,
|
|
362
|
+
tables: matches.slice(0, limit),
|
|
363
|
+
...(errors.length
|
|
364
|
+
? { errors: errors.slice(0, 12), errorCount: errors.length }
|
|
365
|
+
: {}),
|
|
366
|
+
nextStep:
|
|
367
|
+
"Use table=dataset.table for full metadata. Global search is bounded; pass a returned dataset/table reference for a complete inspection.",
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
248
371
|
async function getTableMetadata(
|
|
249
372
|
projectId: string,
|
|
250
373
|
datasetId: string,
|
|
@@ -256,7 +379,7 @@ async function getTableMetadata(
|
|
|
256
379
|
|
|
257
380
|
export default defineAction({
|
|
258
381
|
description:
|
|
259
|
-
"Search or describe BigQuery metadata for the configured warehouse. Use before writing SQL when the data dictionary does not already name the dataset, table, and columns. With no args, lists datasets. With dataset, lists tables. With dataset + table or a dataset.table value, returns columns for that table.",
|
|
382
|
+
"Search or describe BigQuery metadata for the configured warehouse. Use before writing SQL when the data dictionary does not already name the dataset, table, and columns. With no args, lists datasets. With search and no dataset, searches accessible tables and columns across the configured project so the user does not need to provide internal table names. With dataset, lists tables. With dataset + table or a dataset.table value, returns columns for that table.",
|
|
260
383
|
schema: z.object({
|
|
261
384
|
dataset: z
|
|
262
385
|
.string()
|
|
@@ -307,6 +430,9 @@ export default defineAction({
|
|
|
307
430
|
}
|
|
308
431
|
|
|
309
432
|
if (!args.dataset) {
|
|
433
|
+
if (search) {
|
|
434
|
+
return searchAcrossDatasets(configuredProjectId, search, limit);
|
|
435
|
+
}
|
|
310
436
|
return {
|
|
311
437
|
mode: "datasets",
|
|
312
438
|
projectId: configuredProjectId,
|
|
@@ -46,6 +46,11 @@ import { toast } from "sonner";
|
|
|
46
46
|
|
|
47
47
|
import { useAuth } from "@/components/auth/AuthProvider";
|
|
48
48
|
import { ANALYTICS_CHAT_STORAGE_KEY } from "@/lib/chat-handoff";
|
|
49
|
+
import {
|
|
50
|
+
matchesDashboardVisibilityFilter,
|
|
51
|
+
type DashboardVisibility,
|
|
52
|
+
type DashboardVisibilityFilter,
|
|
53
|
+
} from "@/lib/dashboard-visibility";
|
|
49
54
|
import { cn, shortcutModifierLabel } from "@/lib/utils";
|
|
50
55
|
import {
|
|
51
56
|
dashboards,
|
|
@@ -63,6 +68,7 @@ type SidebarDashboard = {
|
|
|
63
68
|
source: "static" | "sql" | "analysis";
|
|
64
69
|
resourceId?: string;
|
|
65
70
|
visibility?: Visibility;
|
|
71
|
+
ownerEmail?: string | null;
|
|
66
72
|
/** Id of the dashboard this one nests under in the sidebar, if any. */
|
|
67
73
|
parentId?: string;
|
|
68
74
|
};
|
|
@@ -162,7 +168,7 @@ const SIDEBAR_SKELETON_CLASS =
|
|
|
162
168
|
"bg-sidebar-foreground/12 dark:bg-sidebar-foreground/10";
|
|
163
169
|
|
|
164
170
|
type SidebarSortMode = "most-used" | "alphabetical" | "manual";
|
|
165
|
-
type SidebarVisibilityFilter =
|
|
171
|
+
type SidebarVisibilityFilter = DashboardVisibilityFilter;
|
|
166
172
|
|
|
167
173
|
import {
|
|
168
174
|
DndContext,
|
|
@@ -258,14 +264,11 @@ function isVisibility(value: unknown): value is Visibility {
|
|
|
258
264
|
}
|
|
259
265
|
|
|
260
266
|
export function matchesVisibilityFilter(
|
|
261
|
-
item: { visibility?: Visibility },
|
|
267
|
+
item: { visibility?: Visibility; ownerEmail?: string | null },
|
|
262
268
|
filter: SidebarVisibilityFilter,
|
|
269
|
+
currentUserEmail?: string | null,
|
|
263
270
|
): boolean {
|
|
264
|
-
|
|
265
|
-
if (filter === "private") {
|
|
266
|
-
return item.visibility !== "org" && item.visibility !== "public";
|
|
267
|
-
}
|
|
268
|
-
return item.visibility === "org" || item.visibility === "public";
|
|
271
|
+
return matchesDashboardVisibilityFilter(item, filter, currentUserEmail);
|
|
269
272
|
}
|
|
270
273
|
|
|
271
274
|
export function threadMatchesVisibilityFilter(
|
|
@@ -447,7 +450,7 @@ function SidebarSectionSettingsPopover({
|
|
|
447
450
|
|
|
448
451
|
// --- Visibility types and helpers ---
|
|
449
452
|
|
|
450
|
-
type Visibility =
|
|
453
|
+
type Visibility = DashboardVisibility;
|
|
451
454
|
|
|
452
455
|
// --- Shared sortable row (used by both dashboards and analyses) ---
|
|
453
456
|
|
|
@@ -1143,6 +1146,7 @@ type SqlDashboardListItem = {
|
|
|
1143
1146
|
id: string;
|
|
1144
1147
|
name: string;
|
|
1145
1148
|
visibility?: Visibility;
|
|
1149
|
+
ownerEmail?: string | null;
|
|
1146
1150
|
parentId?: string;
|
|
1147
1151
|
};
|
|
1148
1152
|
|
|
@@ -1160,18 +1164,25 @@ async function fetchSqlDashboards(
|
|
|
1160
1164
|
d.visibility === "org" ||
|
|
1161
1165
|
d.visibility === "public"),
|
|
1162
1166
|
)
|
|
1163
|
-
.map((d: any) =>
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1167
|
+
.map((d: any) => {
|
|
1168
|
+
const ownerEmail =
|
|
1169
|
+
typeof d.ownerEmail === "string" && d.ownerEmail.trim().length > 0
|
|
1170
|
+
? d.ownerEmail
|
|
1171
|
+
: undefined;
|
|
1172
|
+
return {
|
|
1173
|
+
id: d.id,
|
|
1174
|
+
name:
|
|
1175
|
+
typeof d.name === "string" && d.name.trim().length > 0
|
|
1176
|
+
? d.name
|
|
1177
|
+
: t("sidebar.untitledDashboard"),
|
|
1178
|
+
visibility: d.visibility as Visibility,
|
|
1179
|
+
...(ownerEmail ? { ownerEmail } : {}),
|
|
1180
|
+
parentId:
|
|
1181
|
+
typeof d.parentId === "string" && d.parentId.trim().length > 0
|
|
1182
|
+
? d.parentId
|
|
1183
|
+
: undefined,
|
|
1184
|
+
};
|
|
1185
|
+
});
|
|
1175
1186
|
}
|
|
1176
1187
|
|
|
1177
1188
|
async function fetchSidebarAnalyses(t: (key: string) => string): Promise<
|
|
@@ -1525,7 +1536,8 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) {
|
|
|
1525
1536
|
activeDashboardId !== null,
|
|
1526
1537
|
);
|
|
1527
1538
|
const [dashShowAll, setDashShowAll] = useState(false);
|
|
1528
|
-
const [dashFilter, setDashFilter] =
|
|
1539
|
+
const [dashFilter, setDashFilter] =
|
|
1540
|
+
useState<SidebarVisibilityFilter>("private");
|
|
1529
1541
|
const [dashboardSortMode, setDashboardSortModeState] =
|
|
1530
1542
|
useState<SidebarSortMode>(() => getStoredSortMode(DASHBOARD_SORT_MODE_KEY));
|
|
1531
1543
|
const { data: popularity, isReady: popularityReady } = usePopularity();
|
|
@@ -1749,6 +1761,7 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) {
|
|
|
1749
1761
|
name: d.name,
|
|
1750
1762
|
source: "sql",
|
|
1751
1763
|
visibility: d.visibility,
|
|
1764
|
+
ownerEmail: d.ownerEmail,
|
|
1752
1765
|
parentId: d.parentId,
|
|
1753
1766
|
}));
|
|
1754
1767
|
const analysisItems: SidebarDashboard[] = analysesList.map((a) => ({
|
|
@@ -1802,9 +1815,9 @@ export function Sidebar({ mobile }: { mobile?: boolean } = {}) {
|
|
|
1802
1815
|
const filteredDashboards = useMemo(
|
|
1803
1816
|
() =>
|
|
1804
1817
|
visibleDashboards.filter((dashboard) =>
|
|
1805
|
-
matchesVisibilityFilter(dashboard, dashFilter),
|
|
1818
|
+
matchesVisibilityFilter(dashboard, dashFilter, auth?.email),
|
|
1806
1819
|
),
|
|
1807
|
-
[visibleDashboards, dashFilter],
|
|
1820
|
+
[auth?.email, visibleDashboards, dashFilter],
|
|
1808
1821
|
);
|
|
1809
1822
|
|
|
1810
1823
|
// Group dashboards that declare a parentId beneath their parent. Nesting is
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type DashboardVisibility = "private" | "org" | "public";
|
|
2
|
+
export type DashboardVisibilityFilter = "all" | "private" | "shared";
|
|
3
|
+
|
|
4
|
+
export type DashboardVisibilityItem = {
|
|
5
|
+
visibility?: DashboardVisibility;
|
|
6
|
+
ownerEmail?: string | null;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function normalizeEmail(email: string | null | undefined): string | null {
|
|
10
|
+
const normalized = email?.trim().toLowerCase();
|
|
11
|
+
return normalized ? normalized : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isDashboardMine(
|
|
15
|
+
item: DashboardVisibilityItem,
|
|
16
|
+
currentUserEmail?: string | null,
|
|
17
|
+
): boolean {
|
|
18
|
+
if (item.visibility === "org" || item.visibility === "public") {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if ("ownerEmail" in item) {
|
|
23
|
+
const ownerEmail = normalizeEmail(item.ownerEmail);
|
|
24
|
+
const viewerEmail = normalizeEmail(currentUserEmail);
|
|
25
|
+
return Boolean(ownerEmail && viewerEmail && ownerEmail === viewerEmail);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Older dashboard-shaped items do not always carry ownership metadata.
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function matchesDashboardVisibilityFilter(
|
|
33
|
+
item: DashboardVisibilityItem,
|
|
34
|
+
filter: DashboardVisibilityFilter,
|
|
35
|
+
currentUserEmail?: string | null,
|
|
36
|
+
): boolean {
|
|
37
|
+
if (filter === "all") return true;
|
|
38
|
+
return filter === "private"
|
|
39
|
+
? isDashboardMine(item, currentUserEmail)
|
|
40
|
+
: !isDashboardMine(item, currentUserEmail);
|
|
41
|
+
}
|
|
@@ -56,6 +56,10 @@ import {
|
|
|
56
56
|
SelectValue,
|
|
57
57
|
} from "@/components/ui/select";
|
|
58
58
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
59
|
+
import {
|
|
60
|
+
isDashboardMine,
|
|
61
|
+
type DashboardVisibility,
|
|
62
|
+
} from "@/lib/dashboard-visibility";
|
|
59
63
|
import { dashboardCacheScope } from "@/lib/prefetch-keys";
|
|
60
64
|
|
|
61
65
|
import { NewDashboardDialog } from "../components/layout/NewDashboardDialog";
|
|
@@ -65,7 +69,8 @@ type Dashboard = {
|
|
|
65
69
|
id: string;
|
|
66
70
|
name?: string;
|
|
67
71
|
title?: string;
|
|
68
|
-
visibility?:
|
|
72
|
+
visibility?: DashboardVisibility;
|
|
73
|
+
ownerEmail?: string | null;
|
|
69
74
|
folderId?: string | null;
|
|
70
75
|
};
|
|
71
76
|
type Folder = {
|
|
@@ -125,10 +130,15 @@ export default function DashboardOverview() {
|
|
|
125
130
|
) {
|
|
126
131
|
continue;
|
|
127
132
|
}
|
|
133
|
+
const ownerEmail =
|
|
134
|
+
typeof dashboard.ownerEmail === "string"
|
|
135
|
+
? dashboard.ownerEmail
|
|
136
|
+
: undefined;
|
|
128
137
|
validDashboards.push({
|
|
129
138
|
id: dashboard.id,
|
|
130
139
|
name: dashboard.name,
|
|
131
140
|
visibility: dashboard.visibility,
|
|
141
|
+
...(ownerEmail ? { ownerEmail } : {}),
|
|
132
142
|
folderId: dashboard.folderId ?? null,
|
|
133
143
|
});
|
|
134
144
|
}
|
|
@@ -394,6 +404,7 @@ export default function DashboardOverview() {
|
|
|
394
404
|
folders={folders}
|
|
395
405
|
grouped={grouped}
|
|
396
406
|
onMove={moveDashboard}
|
|
407
|
+
currentUserEmail={auth?.email}
|
|
397
408
|
t={t}
|
|
398
409
|
/>
|
|
399
410
|
))}
|
|
@@ -428,6 +439,7 @@ function DashboardScopeSection({
|
|
|
428
439
|
folders,
|
|
429
440
|
grouped,
|
|
430
441
|
onMove,
|
|
442
|
+
currentUserEmail,
|
|
431
443
|
t,
|
|
432
444
|
}: {
|
|
433
445
|
scope: Scope;
|
|
@@ -435,11 +447,12 @@ function DashboardScopeSection({
|
|
|
435
447
|
folders: Folder[];
|
|
436
448
|
grouped: Map<string, Dashboard[]>;
|
|
437
449
|
onMove: (dashboard: Dashboard, folderId: string | null) => void;
|
|
450
|
+
currentUserEmail?: string | null;
|
|
438
451
|
t: ReturnType<typeof useT>;
|
|
439
452
|
}) {
|
|
440
453
|
const scopeFolders = folders.filter((folder) => folder.scope === scope);
|
|
441
454
|
const scopeDashboards = dashboards.filter((dashboard) =>
|
|
442
|
-
isDashboardInScope(dashboard, scope),
|
|
455
|
+
isDashboardInScope(dashboard, scope, currentUserEmail),
|
|
443
456
|
);
|
|
444
457
|
const Icon = scope === "personal" ? IconLock : IconUsers;
|
|
445
458
|
|
|
@@ -476,6 +489,7 @@ function DashboardScopeSection({
|
|
|
476
489
|
folders={folders}
|
|
477
490
|
scope={scope}
|
|
478
491
|
onMove={onMove}
|
|
492
|
+
currentUserEmail={currentUserEmail}
|
|
479
493
|
t={t}
|
|
480
494
|
/>
|
|
481
495
|
))}
|
|
@@ -485,6 +499,7 @@ function DashboardScopeSection({
|
|
|
485
499
|
folders={folders}
|
|
486
500
|
scope={scope}
|
|
487
501
|
onMove={onMove}
|
|
502
|
+
currentUserEmail={currentUserEmail}
|
|
488
503
|
t={t}
|
|
489
504
|
hideWhenEmpty
|
|
490
505
|
/>
|
|
@@ -500,6 +515,7 @@ function FolderSection({
|
|
|
500
515
|
folders,
|
|
501
516
|
scope,
|
|
502
517
|
onMove,
|
|
518
|
+
currentUserEmail,
|
|
503
519
|
t,
|
|
504
520
|
hideWhenEmpty = false,
|
|
505
521
|
}: {
|
|
@@ -508,11 +524,12 @@ function FolderSection({
|
|
|
508
524
|
folders: Folder[];
|
|
509
525
|
scope: Scope;
|
|
510
526
|
onMove: (dashboard: Dashboard, folderId: string | null) => void;
|
|
527
|
+
currentUserEmail?: string | null;
|
|
511
528
|
t: ReturnType<typeof useT>;
|
|
512
529
|
hideWhenEmpty?: boolean;
|
|
513
530
|
}) {
|
|
514
531
|
const scopedDashboards = dashboards.filter((dashboard) =>
|
|
515
|
-
isDashboardInScope(dashboard, scope),
|
|
532
|
+
isDashboardInScope(dashboard, scope, currentUserEmail),
|
|
516
533
|
);
|
|
517
534
|
if (hideWhenEmpty && scopedDashboards.length === 0) return null;
|
|
518
535
|
|
|
@@ -540,6 +557,7 @@ function FolderSection({
|
|
|
540
557
|
folders={folders}
|
|
541
558
|
currentFolderId={folder?.id ?? null}
|
|
542
559
|
onMove={onMove}
|
|
560
|
+
currentUserEmail={currentUserEmail}
|
|
543
561
|
t={t}
|
|
544
562
|
/>
|
|
545
563
|
))}
|
|
@@ -554,23 +572,26 @@ function DashboardRow({
|
|
|
554
572
|
folders,
|
|
555
573
|
currentFolderId,
|
|
556
574
|
onMove,
|
|
575
|
+
currentUserEmail,
|
|
557
576
|
t,
|
|
558
577
|
}: {
|
|
559
578
|
dashboard: Dashboard;
|
|
560
579
|
folders: Folder[];
|
|
561
580
|
currentFolderId: string | null;
|
|
562
581
|
onMove: (dashboard: Dashboard, folderId: string | null) => void;
|
|
582
|
+
currentUserEmail?: string | null;
|
|
563
583
|
t: ReturnType<typeof useT>;
|
|
564
584
|
}) {
|
|
565
585
|
const name =
|
|
566
586
|
dashboard.name || dashboard.title || t("sidebar.untitledDashboard");
|
|
587
|
+
const mine = isDashboardMine(dashboard, currentUserEmail);
|
|
567
588
|
const candidateFolders = folders.filter((folder) => {
|
|
568
|
-
if (
|
|
589
|
+
if (mine) {
|
|
569
590
|
return folder.scope === "personal";
|
|
570
591
|
}
|
|
571
592
|
return dashboard.visibility === "org" && folder.scope === "shared";
|
|
572
593
|
});
|
|
573
|
-
const canOrganize = dashboard.visibility
|
|
594
|
+
const canOrganize = mine || dashboard.visibility === "org";
|
|
574
595
|
|
|
575
596
|
return (
|
|
576
597
|
<div className="group flex items-center gap-3 px-4 py-3 transition-colors hover:bg-muted/30">
|
|
@@ -586,7 +607,7 @@ function DashboardRow({
|
|
|
586
607
|
{name}
|
|
587
608
|
</p>
|
|
588
609
|
<p className="truncate text-xs text-muted-foreground">
|
|
589
|
-
{
|
|
610
|
+
{mine
|
|
590
611
|
? t("dashboardOverview.personal")
|
|
591
612
|
: t("dashboardOverview.shared")}
|
|
592
613
|
</p>
|
|
@@ -635,8 +656,12 @@ function DashboardRow({
|
|
|
635
656
|
);
|
|
636
657
|
}
|
|
637
658
|
|
|
638
|
-
function isDashboardInScope(
|
|
659
|
+
function isDashboardInScope(
|
|
660
|
+
dashboard: Dashboard,
|
|
661
|
+
scope: Scope,
|
|
662
|
+
currentUserEmail?: string | null,
|
|
663
|
+
): boolean {
|
|
639
664
|
return scope === "personal"
|
|
640
|
-
? dashboard
|
|
641
|
-
: dashboard
|
|
665
|
+
? isDashboardMine(dashboard, currentUserEmail)
|
|
666
|
+
: !isDashboardMine(dashboard, currentUserEmail);
|
|
642
667
|
}
|
|
@@ -286,10 +286,13 @@ function looksLikeWorkflowOrAutomationRequest(lower: string): boolean {
|
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
const ANALYTICS_RESULT_TERMS =
|
|
289
|
-
/\b(conversion|conversions|funnel|revenue|payment|payments|traffic|pageviews?|signups?|events?|active users?|sessions?|retention|churn|pipeline|deals?|calls?|transcripts?|sentiment|themes?|objections?|cohorts?|segments?|accounts?|customers?|tickets?|issues?|leads?|opportunities|mrr|arr|ctr|cvr|cac|ltv)\b/;
|
|
289
|
+
/\b(conversion|conversions|funnel|revenue|payment|payments|traffic|pageviews?|signups?|events?|active users?|sessions?|retention|churn|pipeline|deals?|calls?|transcripts?|sentiment|themes?|objections?|cohorts?|segments?|accounts?|customers?|tickets?|issues?|leads?|opportunities|usage|adoption|ai credits?|credit consumption|credits? consumed|allowance|quota|mrr|arr|ctr|cvr|cac|ltv)\b/;
|
|
290
|
+
|
|
291
|
+
const ANALYTICS_DOMAIN_ENTITY_TERMS =
|
|
292
|
+
/\b(?:branch(?:es)?|branch creation|created by|creator identity)\b/;
|
|
290
293
|
|
|
291
294
|
const ANALYTICS_INTENT_TERMS =
|
|
292
|
-
/\b(analy[sz]e|measure|calculate|query|report|summari[sz]e|break ?down|compare|rank|segment|forecast|trend|count|total|average|median|percent(?:age)?|rate|top|bottom|highest|lowest|how many|how much|what (?:is|are|was|were)|which|why)\b/;
|
|
295
|
+
/\b(analy[sz]e|measure|calculate|query|report|summari[sz]e|break ?down|compare|rank|segment|forecast|trend|count|total|average|median|percent(?:age)?|rate|top|bottom|highest|lowest|pull|fetch|retrieve|export|show|how many|how much|what (?:is|are|was|were)|which|why)\b/;
|
|
293
296
|
|
|
294
297
|
const SOURCE_SEARCH_INTENT_TERMS =
|
|
295
298
|
/\b(find|surface|search|scan|grep|review|inspect|check|look through|go find)\b/;
|
|
@@ -308,7 +311,7 @@ const ARTIFACT_DATA_INTENT =
|
|
|
308
311
|
// a live provider data call. They should be answered from the data dictionary,
|
|
309
312
|
// schema introspection tools, or the agent's knowledge of configured sources.
|
|
310
313
|
const METADATA_ONLY_TERMS =
|
|
311
|
-
/\b(what (?:tables?|columns?|fields?|sources?|datasets?|metrics?|schema) (?:are|is|exist|available|do (?:we|you|i) have)|which (?:sources?|tables?|providers?|integrations?) (?:are|is) (?:connected|configured|available|set up)|list (?:the )?(?:tables?|columns?|fields?|sources?|datasets?|schemas?)|show (?:me )?(?:available|the) (?:sources?|tables?|schemas?)|what does .+ (?:mean|measure|represent|track)|how is .+ (?:defined|calculated|computed|measured)|definition of|describe (?:the )?(?:\w+\s+)?(?:table|column|schema|metric|field)|list (?:the )?columns?\s+in|what (?:is|are) (?:the )?(?:data (?:dictionary|schema)|available (?:sources?|tables?))|what (?:source|provider|table) (?:has|stores|contains))\b/;
|
|
314
|
+
/\b(what (?:tables?|columns?|fields?|sources?|datasets?|metrics?|schema) (?:are|is|exist|available|do (?:we|you|i) have)|which (?:sources?|tables?|providers?|integrations?) (?:are|is) (?:connected|configured|available|set up)|list (?:the )?(?:tables?|columns?|fields?|sources?|datasets?|schemas?)|show (?:me )?(?:available|the) (?:data )?(?:sources?|tables?|schemas?)|what does .+ (?:mean|measure|represent|track)|how is .+ (?:defined|calculated|computed|measured)|definition of|describe (?:the )?(?:\w+\s+)?(?:table|column|schema|metric|field)|list (?:the )?columns?\s+in|what (?:is|are) (?:the )?(?:data (?:dictionary|schema)|available (?:data )?(?:sources?|tables?))|what (?:source|provider|table) (?:has|stores|contains))\b/;
|
|
312
315
|
|
|
313
316
|
export function looksLikeAnalyticsDataRequest(text: string): boolean {
|
|
314
317
|
const requestText = stripInjectedAnalyticsGuardContext(text);
|
|
@@ -350,6 +353,14 @@ export function looksLikeAnalyticsDataRequest(text: string): boolean {
|
|
|
350
353
|
if (METADATA_ONLY_TERMS.test(lower)) return false;
|
|
351
354
|
|
|
352
355
|
if (ANALYTICS_RESULT_TERMS.test(lower)) return true;
|
|
356
|
+
if (
|
|
357
|
+
ANALYTICS_DOMAIN_ENTITY_TERMS.test(lower) &&
|
|
358
|
+
(ANALYTICS_INTENT_TERMS.test(lower) ||
|
|
359
|
+
SOURCE_SEARCH_INTENT_TERMS.test(lower) ||
|
|
360
|
+
/\bdata\b/.test(lower))
|
|
361
|
+
) {
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
353
364
|
if (
|
|
354
365
|
ANALYTICS_INTENT_TERMS.test(lower) &&
|
|
355
366
|
/\b(data|source|table|sql)\b/.test(lower)
|