Skip to content

Commit 583c176

Browse files
Dashboard explorer (#298)
* let dashcard be the base card, handle limit entities * add parametervalues * link render bug fix * new tool in app controller * add mbql via dataset * add continue analysis * Specify if no data returned in query execution * Continue MBQL threads from dashboard * Allow user to continue old threads --------- Co-authored-by: nuwandavek <vivekaithal44@gmail.com>
1 parent a5bb81b commit 583c176

File tree

16 files changed

+473
-250
lines changed

16 files changed

+473
-250
lines changed

apps/src/base/appController.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { RPCs, utils } from "web";
44
import { ActionRenderInfo, DefaultMessageContent } from "web/types"
55
import 'reflect-metadata';
66

7-
interface App<T> {
7+
export interface App<T> {
88
getState: () => Promise<T>;
99
getQuerySelectorMap: () => Promise<QuerySelectorMap>;
1010
}

apps/src/metabase/appController.ts

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import {
77
MetabaseAppStateSQLEditor,
88
MetabaseSemanticQueryAppState,
99
MetabaseAppStateMBQLEditor,
10-
MetabasePageType,
1110
} from "./helpers/DOMToState";
1211

12+
import { MetabasePageType } from "./helpers/utils";
13+
1314
import { MetabaseAppStateSQLEditorV2 } from "./helpers/analystModeTypes";
1415

1516
import {
@@ -45,7 +46,7 @@ import {
4546
} from "./helpers/sqlQuery";
4647
import axios from 'axios'
4748
import { getSelectedDbId, getCurrentUserInfo as getUserInfo, getSnippets, getCurrentCard, getDashboardState, getCurrentQuery, getParameterValues } from "./helpers/metabaseStateAPI";
48-
import { runSQLQueryFromDashboard } from "./helpers/dashboard/runSqlQueryFromDashboard";
49+
import { runSQLQueryFromDashboard, runMBQLQueryFromDashboard } from "./helpers/dashboard/runSqlQueryFromDashboard";
4950
import { getAllRelevantModelsForSelectedDb, getTableData } from "./helpers/metabaseAPIHelpers";
5051
import { processSQLWithCtesOrModels, dispatch, updateIsDevToolsOpen, updateDevToolsTabName, addMemory } from "web";
5152
import { fetchTableMetadata } from "./helpers/metabaseAPI";
@@ -266,6 +267,20 @@ export class MetabaseController extends AppController<MetabaseAppState> {
266267
return actionContent;
267268
}
268269

270+
async runMBQLQuery({ mbql, dbID }: { mbql: any, dbID: number }) {
271+
const actionContent: BlankMessageContent = {
272+
type: "BLANK",
273+
};
274+
const response = await runMBQLQueryFromDashboard(mbql, dbID);
275+
if (response.error) {
276+
actionContent.content = `<ERROR>${response.error}</ERROR>`;
277+
} else {
278+
const asMarkdown = metabaseToCSV(response.data);
279+
actionContent.content = asMarkdown;
280+
}
281+
return actionContent;
282+
}
283+
269284
@Action({
270285
labelRunning: "Running SQL Query with parameters",
271286
labelDone: "Ran SQL query with parameters",
@@ -560,6 +575,64 @@ export class MetabaseController extends AppController<MetabaseAppState> {
560575
return actionContent;
561576
}
562577

578+
@Action({
579+
labelRunning: "Constructs the MBQL query",
580+
labelDone: "MBQL built",
581+
labelTask: "Built MBQL query",
582+
description: "Constructs the MBQL query in the GUI editor",
583+
renderBody: ({ mbql, explanation }: { mbql: any, explanation: string }) => {
584+
if (isEmpty(mbql)) {
585+
return {text: "This MBQL query has errors", code: null, language: "markdown"}
586+
}
587+
return {text: explanation, code: JSON.stringify(mbql), language: "json"}
588+
}
589+
})
590+
async ExecuteMBQLQuery({ mbql, explanation }: { mbql: any, explanation: string }) {
591+
const actionContent: BlankMessageContent = {
592+
type: "BLANK",
593+
};
594+
const state = (await this.app.getState()) as MetabaseAppStateMBQLEditor;
595+
const dbID = state?.selectedDatabaseInfo?.id as number
596+
if (!dbID) {
597+
actionContent.content = "No database selected";
598+
return actionContent;
599+
}
600+
if (isEmpty(mbql)) {
601+
actionContent.content = "This MBQL query has errors: " + explanation;
602+
return actionContent;
603+
}
604+
605+
if (mbql) {
606+
const table_ids = getSourceTableIds(mbql);
607+
await updateMBEntities(table_ids)
608+
}
609+
610+
const finCard = {
611+
type: "question",
612+
visualization_settings: {},
613+
display: "table",
614+
dataset_query: {
615+
database: dbID,
616+
type: "query",
617+
query: mbql
618+
}
619+
};
620+
621+
const metabaseState = this.app as App<MetabaseAppState>;
622+
const pageType = metabaseState.useStore().getState().toolContext?.pageType as MetabasePageType;
623+
if (pageType === 'mbql') {
624+
// # Ensure you're in mbql editor mode
625+
await RPCs.dispatchMetabaseAction('metabase/qb/SET_UI_CONTROLS', {
626+
queryBuilderMode: "notebook",
627+
});
628+
await RPCs.dispatchMetabaseAction('metabase/qb/UPDATE_QUESTION', {card: finCard});
629+
return await this._executeMBQLQueryInternal()
630+
}
631+
else if ((pageType === 'dashboard') || (pageType === 'unknown')) {
632+
return await this.runMBQLQuery({mbql, dbID});
633+
}
634+
}
635+
563636
@Action({
564637
labelRunning: "Constructs the MBQL query",
565638
labelDone: "MBQL built",

apps/src/metabase/helpers/DOMToState.ts

Lines changed: 2 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { MBQLInfo } from './mbql/utils';
1919
import { getModelsWithFields, getSelectedAndRelevantModels, modifySqlForMetabaseModels} from './metabaseModels';
2020
import { MetabaseAppStateSQLEditorV2, MetabaseAppStateType, processCard } from './analystModeTypes';
2121
import { MetabaseTableOrModel } from './metabaseAPITypes';
22-
import { determineMetabasePageType, MetabasePageType } from './utils';
22+
import { determineMetabasePageType, MetabasePageType, getLimitedEntities } from './utils';
2323

2424
const {modifySqlForMxModels} = catalogAsModels
2525

@@ -80,6 +80,7 @@ export interface MetabaseAppStateDashboard extends DashboardInfo {
8080
metabaseUrl?: string;
8181
isEmbedded: boolean;
8282
limitedEntities?: MetabaseTableOrModel[];
83+
parameterValues?: ParameterValues;
8384
}
8485

8586
export interface MetabaseAppStateMBQLEditor extends MBQLInfo {
@@ -105,62 +106,6 @@ export interface MetabaseSemanticQueryAppState {
105106
export type MetabaseAppState = MetabaseAppStateSQLEditor | MetabaseAppStateDashboard | MetabaseSemanticQueryAppState | MetabaseAppStateMBQLEditor | MetabaseAppStateSQLEditorV2;
106107

107108
// no need to fetch fields since we don't want that in limited entities
108-
async function getLimitedEntities(sqlQuery: string): Promise<MetabaseTableOrModel[]> {
109-
const appSettings = RPCs.getAppSettings();
110-
111-
// Early return if conditions not met
112-
if (!appSettings.analystMode || !appSettings.manuallyLimitContext) {
113-
return [];
114-
}
115-
116-
const dbId = await getSelectedDbId();
117-
const selectedDatabaseInfo = dbId ? await getDatabaseInfo(dbId) : undefined;
118-
const defaultSchema = selectedDatabaseInfo?.default_schema;
119-
120-
const sqlTables = getTablesFromSqlRegex(sqlQuery);
121-
const selectedCatalogObj = find(appSettings.availableCatalogs, { name: appSettings.selectedCatalog });
122-
const selectedCatalog = get(selectedCatalogObj, 'content');
123-
124-
// Apply default schema to tables if needed
125-
if (defaultSchema) {
126-
sqlTables.forEach((table) => {
127-
if (table.schema === undefined || table.schema === '') {
128-
table.schema = defaultSchema;
129-
}
130-
});
131-
}
132-
133-
let relevantTablesWithFields = await getTablesWithFields(appSettings.tableDiff, appSettings.drMode, !!selectedCatalog, sqlTables, []);
134-
135-
// Add defaultSchema back to relevantTablesWithFields
136-
relevantTablesWithFields = relevantTablesWithFields.map(table => {
137-
if (table.schema === undefined || table.schema === '') {
138-
table.schema = defaultSchema || 'unknown';
139-
}
140-
return table;
141-
});
142-
143-
const allModels = dbId ? await getAllRelevantModelsForSelectedDb(dbId) : [];
144-
const relevantModels = await getSelectedAndRelevantModels(sqlQuery || "", appSettings.selectedModels, allModels);
145-
146-
// Transform and combine tables and models with type annotations
147-
const relevantTablesWithFieldsAndType: MetabaseTableOrModel[] = relevantTablesWithFields.map(table => ({
148-
type: 'table',
149-
id: table.id,
150-
name: table.name,
151-
schema: table.schema,
152-
description: table.description,
153-
}));
154-
155-
const relevantModelsWithFieldsAndType: MetabaseTableOrModel[] = relevantModels.map(model => ({
156-
type: 'model',
157-
id: model.modelId || 0,
158-
name: model.name,
159-
description: model.description,
160-
}));
161-
162-
return [...relevantTablesWithFieldsAndType, ...relevantModelsWithFieldsAndType];
163-
}
164109

165110
export async function convertDOMtoStateSQLQueryV2(pageType: MetabasePageType) : Promise<MetabaseAppStateSQLEditorV2> {
166111
const [metabaseUrl, currentCardRaw, outputMarkdown, parameterValues] = await Promise.all([

apps/src/metabase/helpers/dashboard/appState.ts

Lines changed: 32 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import { getTableContextYAML } from '../catalog';
1313
import { getModelsFromSql, getModelsWithFields, modifySqlForMetabaseModels, replaceLLMFriendlyIdentifiersInSqlWithModels } from '../metabaseModels';
1414
import { MetabaseAppStateType } from '../analystModeTypes';
1515
import { MetabaseTableOrModel } from '../metabaseAPITypes';
16+
import { processCard } from '../analystModeTypes';
17+
import { Card, SavedCard } from '../types';
18+
import { getLimitedEntitiesFromQueries, getLimitedEntitiesFromMBQLQueries } from '../utils';
1619

1720
// Removed: const { getMetabaseState } = RPCs - using centralized state functions instead
1821

@@ -77,14 +80,7 @@ export type DashboardInfoForModelling = {
7780
type: string,
7881
value?: string | null
7982
}[];
80-
cards: {
81-
id: number,
82-
name: string,
83-
sql: string,
84-
databaseId: number,
85-
description?: string | undefined,
86-
outputTableMarkdown?: string,
87-
}[]
83+
cards: SavedCard[]
8884
}
8985

9086
function substituteParameterMappings(
@@ -103,65 +99,30 @@ function substituteParameterMappings(
10399
return sql
104100
}
105101

106-
async function getDashcardInfoWithSQLAndOutputTableMd(
102+
async function getDashcardwithOutputTableMd(
107103
dashboardMetabaseState: DashboardMetabaseState,
108104
dashcardId: number,
109105
dashboardId: number): Promise<DashboardInfoForModelling['cards'][number] | null> {
110-
const dashcard = dashboardMetabaseState.dashcards[dashcardId];
106+
const dashcard = dashboardMetabaseState.dashcards[dashcardId].card;
111107
if (!dashcard) {
112108
return null;
113109
}
114-
const cardId = _.get(dashcard, 'card_id', '');
115-
const databaseId = _.get(dashcard, 'card.database_id', 0);
116-
const id = _.get(dashcard, 'id');
117-
const query_type = _.get(dashcard, 'card.query_type', 'unknown');
118-
const name = _.get(dashcard, 'card.name', '');
119-
const description = _.get(dashcard, 'card.description', '');
120-
const visualizationType = _.get(dashcard, 'card.display', '');
121-
if (!name)
110+
const cardID = _.get(dashcard, 'id', null);
111+
if (!cardID) {
122112
return null;
123-
let sql = ''
124-
if (query_type == 'native'){
125-
sql = _.get(dashcard, 'card.dataset_query.native.query', '');
126-
}
127-
else if (query_type == 'query'){
128-
// mbql query
129-
const mbqlQuery = _.get(dashcard, 'card.dataset_query.query', {});
130-
if (mbqlQuery) {
131-
const sql_from_mbql = await getSQLFromMBQL({
132-
database: databaseId,
133-
type: 'query',
134-
query: mbqlQuery
135-
});
136-
sql = sql_from_mbql.query || '';
137-
}
138113
}
139-
140-
if (!sql || sql == '')
141-
return null;
114+
const card = processCard(dashcard) as SavedCard;
115+
card.id = cardID;
142116

143-
// replace parameters
144-
try {
145-
sql = await substituteParameters(sql, dashcard, dashboardMetabaseState['dashboards'][dashboardId]?.param_fields, dashboardMetabaseState.parameterValues)
146-
} catch (e) {
147-
}
148-
const obj = {
149-
id,
150-
name,
151-
sql,
152-
databaseId,
153-
visualizationType,
154-
...(description ? { description } : {}),
155-
}
156117
// dashcardData
157-
const data = _.get(dashboardMetabaseState, ['dashcardData', dashcardId, cardId, 'data']);
118+
const data = _.get(dashboardMetabaseState, ['dashcardData', dashcardId, card.id, 'data']);
158119
if (!data) {
159-
return obj
120+
return card
160121
}
161122
const dataAsMarkdown = metabaseToMarkdownTable(data, 1000);
162123
return {
163-
...obj,
164-
outputTableMarkdown: dataAsMarkdown
124+
...card,
125+
outputTableMarkdown: dataAsMarkdown
165126
}
166127
}
167128
/*
@@ -278,75 +239,33 @@ export async function getDashboardAppState(): Promise<MetabaseAppStateDashboard
278239
}
279240
const selectedTabDashcardIds = getSelectedTabDashcardIds(dashboardMetabaseState);
280241
// const dashboardParameters = _.get(dashboardMetabaseState, ['dashboards', dashboardId, 'parameters'], [])
281-
const allModels = dbId ? await getAllRelevantModelsForSelectedDb(dbId) : []
282-
const cards = await Promise.all(selectedTabDashcardIds.map(async dashcardId => await getDashcardInfoWithSQLAndOutputTableMd(dashboardMetabaseState, dashcardId, dashboardId, allModels)))
242+
const cards = await Promise.all(selectedTabDashcardIds.map(async dashcardId => await getDashcardwithOutputTableMd(dashboardMetabaseState, dashcardId, dashboardId)))
283243
let filteredCards = _.compact(cards);
284-
let sqlTables: TableAndSchema[] = []
285-
forEach(filteredCards, (card) => {
286-
if (card) {
287-
getTablesFromSqlRegex(card.sql).forEach((table) => {
288-
if (defaultSchema) {
289-
if (table.schema === undefined || table.schema === '') {
290-
table.schema = defaultSchema
291-
}
292-
}
293-
sqlTables.push(table)
294-
})
295-
}
296-
})
297-
298-
299-
sqlTables = _.uniqBy(sqlTables, (table) => `${table.schema}::${table.name}`)
300-
const relevantTablesWithFields = await getTablesWithFields(appSettings.tableDiff, appSettings.drMode, !!selectedCatalog, sqlTables, [])
301-
// find a list of models from each native card using getModelsFromSql, and then merge them to get relevantModels
302-
const modelsFromAllCards = (await Promise.all(filteredCards.map(async card => {
303-
if (card.sql) {
304-
return await getModelsFromSql(card.sql, allModels)
305-
}
306-
return []
307-
}))).flat()
308-
const dedupedCardAndSelectedModels = _.uniqBy([...modelsFromAllCards, ...appSettings.selectedModels], 'modelId')
309-
const relevantModelsWithFields = await getModelsWithFields(dedupedCardAndSelectedModels)
310-
const allFormattedTables = [...relevantTablesWithFields, ...relevantModelsWithFields]
311-
const tableContextYAML = getTableContextYAML(allFormattedTables, selectedCatalog, appSettings.drMode);
312-
filteredCards = filteredCards.map(card => {
313-
// replace model identifiers with model ids
314-
card.sql = modifySqlForMetabaseModels(card.sql, allModels)
315-
return card
316-
})
317-
dashboardInfo.cards = filteredCards
318-
// filter out dashcards with null names or ids
319-
.filter(dashcard => dashcard.name !== null && dashcard.id !== null);
320-
// remove description if it's null or undefined
321-
if (!dashboardInfo.description) {
322-
delete dashboardInfo.description;
323-
}
244+
const limitedEntitiesSQL = await getLimitedEntitiesFromQueries(
245+
filteredCards.flatMap(card =>
246+
card?.dataset_query?.native?.query ? [card.dataset_query.native.query] : []
247+
)
248+
);
249+
const limitedEntitiesMBQL = await getLimitedEntitiesFromMBQLQueries(
250+
filteredCards.flatMap(card =>
251+
card?.dataset_query?.query ? [card.dataset_query.query] : []
252+
)
253+
);
254+
const limitedEntities = [...limitedEntitiesSQL, ...limitedEntitiesMBQL];
255+
// remove duplicates based on id and type
256+
const uniqueEntities = Array.from(new Map(limitedEntities.map(entity => [entity.id, entity])).values());
324257
const dashboardAppState: MetabaseAppStateDashboard = {
325258
...dashboardInfo,
326259
type: MetabaseAppStateType.Dashboard,
327-
tableContextYAML,
328260
selectedDatabaseInfo,
329261
metabaseOrigin: url,
330262
metabaseUrl: fullUrl,
331263
isEmbedded: getParsedIframeInfo().isEmbedded,
332264
};
333-
if (appSettings.analystMode && appSettings.manuallyLimitContext) {
334-
const limitToTables: MetabaseTableOrModel[] = relevantTablesWithFields.map(table => ({
335-
type: 'table',
336-
id: table.id,
337-
name: table.name,
338-
schema: table.schema,
339-
description: table.description,
340-
}))
341-
const limitToModels: MetabaseTableOrModel[] = dedupedCardAndSelectedModels.map(model => ({
342-
type: 'model',
343-
id: model.modelId,
344-
name: model.name,
345-
description: model.description,
346-
}))
347-
dashboardAppState.limitedEntities = [...limitToTables, ...limitToModels];
348-
}
349-
return dashboardAppState
265+
dashboardAppState.cards = filteredCards as SavedCard[];
266+
dashboardAppState.limitedEntities = uniqueEntities;
267+
dashboardAppState.parameterValues = dashboardMetabaseState.parameterValues || {};
268+
return dashboardAppState;
350269
}
351270

352271

0 commit comments

Comments
 (0)