@finos/legend-query-builder 4.19.12 → 4.19.13

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.
Files changed (118) hide show
  1. package/lib/__lib__/QueryBuilderDocumentation.d.ts +2 -1
  2. package/lib/__lib__/QueryBuilderDocumentation.d.ts.map +1 -1
  3. package/lib/__lib__/QueryBuilderDocumentation.js +1 -0
  4. package/lib/__lib__/QueryBuilderDocumentation.js.map +1 -1
  5. package/lib/__lib__/QueryBuilderEvent.d.ts +45 -1
  6. package/lib/__lib__/QueryBuilderEvent.d.ts.map +1 -1
  7. package/lib/__lib__/QueryBuilderEvent.js +48 -0
  8. package/lib/__lib__/QueryBuilderEvent.js.map +1 -1
  9. package/lib/__lib__/QueryBuilderTelemetryHelper.d.ts +162 -8
  10. package/lib/__lib__/QueryBuilderTelemetryHelper.d.ts.map +1 -1
  11. package/lib/__lib__/QueryBuilderTelemetryHelper.js +48 -0
  12. package/lib/__lib__/QueryBuilderTelemetryHelper.js.map +1 -1
  13. package/lib/components/QueryBuilder.d.ts.map +1 -1
  14. package/lib/components/QueryBuilder.js +9 -5
  15. package/lib/components/QueryBuilder.js.map +1 -1
  16. package/lib/components/QueryBuilderConstantExpressionPanel.d.ts.map +1 -1
  17. package/lib/components/QueryBuilderConstantExpressionPanel.js +18 -1
  18. package/lib/components/QueryBuilderConstantExpressionPanel.js.map +1 -1
  19. package/lib/components/QueryBuilderParametersPanel.d.ts.map +1 -1
  20. package/lib/components/QueryBuilderParametersPanel.js +18 -1
  21. package/lib/components/QueryBuilderParametersPanel.js.map +1 -1
  22. package/lib/components/QueryBuilderSideBar.d.ts.map +1 -1
  23. package/lib/components/QueryBuilderSideBar.js +19 -0
  24. package/lib/components/QueryBuilderSideBar.js.map +1 -1
  25. package/lib/components/fetch-structure/QueryBuilderGraphFetchTreePanel.d.ts.map +1 -1
  26. package/lib/components/fetch-structure/QueryBuilderGraphFetchTreePanel.js +42 -1
  27. package/lib/components/fetch-structure/QueryBuilderGraphFetchTreePanel.js.map +1 -1
  28. package/lib/components/fetch-structure/QueryBuilderPostFilterPanel.d.ts.map +1 -1
  29. package/lib/components/fetch-structure/QueryBuilderPostFilterPanel.js +25 -5
  30. package/lib/components/fetch-structure/QueryBuilderPostFilterPanel.js.map +1 -1
  31. package/lib/components/fetch-structure/QueryBuilderResultModifierPanel.d.ts.map +1 -1
  32. package/lib/components/fetch-structure/QueryBuilderResultModifierPanel.js +55 -0
  33. package/lib/components/fetch-structure/QueryBuilderResultModifierPanel.js.map +1 -1
  34. package/lib/components/fetch-structure/QueryBuilderTDSPanel.d.ts.map +1 -1
  35. package/lib/components/fetch-structure/QueryBuilderTDSPanel.js +95 -8
  36. package/lib/components/fetch-structure/QueryBuilderTDSPanel.js.map +1 -1
  37. package/lib/components/fetch-structure/QueryBuilderTDSWindowPanel.d.ts.map +1 -1
  38. package/lib/components/fetch-structure/QueryBuilderTDSWindowPanel.js +37 -4
  39. package/lib/components/fetch-structure/QueryBuilderTDSWindowPanel.js.map +1 -1
  40. package/lib/components/filter/QueryBuilderFilterPanel.d.ts.map +1 -1
  41. package/lib/components/filter/QueryBuilderFilterPanel.js +24 -5
  42. package/lib/components/filter/QueryBuilderFilterPanel.js.map +1 -1
  43. package/lib/components/result/QueryBuilderDataProductErrorPanel.d.ts +61 -0
  44. package/lib/components/result/QueryBuilderDataProductErrorPanel.d.ts.map +1 -0
  45. package/lib/components/result/QueryBuilderDataProductErrorPanel.js +176 -0
  46. package/lib/components/result/QueryBuilderDataProductErrorPanel.js.map +1 -0
  47. package/lib/components/result/QueryBuilderResultPanel.d.ts.map +1 -1
  48. package/lib/components/result/QueryBuilderResultPanel.js +24 -18
  49. package/lib/components/result/QueryBuilderResultPanel.js.map +1 -1
  50. package/lib/data-access-overview.css +1 -1
  51. package/lib/index.css +2 -2
  52. package/lib/index.css.map +1 -1
  53. package/lib/index.d.ts +1 -0
  54. package/lib/index.d.ts.map +1 -1
  55. package/lib/index.js +1 -0
  56. package/lib/index.js.map +1 -1
  57. package/lib/package.json +1 -1
  58. package/lib/stores/QueryBuilderResultState.d.ts +11 -1
  59. package/lib/stores/QueryBuilderResultState.d.ts.map +1 -1
  60. package/lib/stores/QueryBuilderResultState.js +79 -15
  61. package/lib/stores/QueryBuilderResultState.js.map +1 -1
  62. package/lib/stores/QueryBuilderState.d.ts +131 -9
  63. package/lib/stores/QueryBuilderState.d.ts.map +1 -1
  64. package/lib/stores/QueryBuilderState.js +124 -21
  65. package/lib/stores/QueryBuilderState.js.map +1 -1
  66. package/lib/stores/QueryBuilder_LegendApplicationPlugin_Extension.d.ts +13 -0
  67. package/lib/stores/QueryBuilder_LegendApplicationPlugin_Extension.d.ts.map +1 -1
  68. package/lib/stores/QueryLoaderState.d.ts +33 -5
  69. package/lib/stores/QueryLoaderState.d.ts.map +1 -1
  70. package/lib/stores/QueryLoaderState.js +10 -1
  71. package/lib/stores/QueryLoaderState.js.map +1 -1
  72. package/lib/stores/data-access/DataProductAccessInfo.d.ts +37 -0
  73. package/lib/stores/data-access/DataProductAccessInfo.d.ts.map +1 -0
  74. package/lib/stores/data-access/DataProductAccessInfo.js +17 -0
  75. package/lib/stores/data-access/DataProductAccessInfo.js.map +1 -0
  76. package/lib/stores/data-cube/QueryBuilderDataCubeEngine.js +1 -1
  77. package/lib/stores/data-cube/QueryBuilderDataCubeEngine.js.map +1 -1
  78. package/lib/stores/explorer/QueryBuilderExplorerState.js +1 -1
  79. package/lib/stores/explorer/QueryBuilderExplorerState.js.map +1 -1
  80. package/lib/stores/milestoning/QueryBuilderMilestoningHelper.d.ts.map +1 -1
  81. package/lib/stores/milestoning/QueryBuilderMilestoningHelper.js +34 -9
  82. package/lib/stores/milestoning/QueryBuilderMilestoningHelper.js.map +1 -1
  83. package/lib/stores/milestoning/QueryBuilderMilestoningState.d.ts +5 -0
  84. package/lib/stores/milestoning/QueryBuilderMilestoningState.d.ts.map +1 -1
  85. package/lib/stores/milestoning/QueryBuilderMilestoningState.js +13 -0
  86. package/lib/stores/milestoning/QueryBuilderMilestoningState.js.map +1 -1
  87. package/lib/stores/workflows/dataProduct/DataProductQueryBuilderState.d.ts +2 -0
  88. package/lib/stores/workflows/dataProduct/DataProductQueryBuilderState.d.ts.map +1 -1
  89. package/lib/stores/workflows/dataProduct/DataProductQueryBuilderState.js +50 -1
  90. package/lib/stores/workflows/dataProduct/DataProductQueryBuilderState.js.map +1 -1
  91. package/package.json +10 -10
  92. package/src/__lib__/QueryBuilderDocumentation.ts +1 -0
  93. package/src/__lib__/QueryBuilderEvent.ts +52 -0
  94. package/src/__lib__/QueryBuilderTelemetryHelper.ts +292 -8
  95. package/src/components/QueryBuilder.tsx +31 -23
  96. package/src/components/QueryBuilderConstantExpressionPanel.tsx +24 -2
  97. package/src/components/QueryBuilderParametersPanel.tsx +25 -2
  98. package/src/components/QueryBuilderSideBar.tsx +28 -0
  99. package/src/components/fetch-structure/QueryBuilderGraphFetchTreePanel.tsx +56 -1
  100. package/src/components/fetch-structure/QueryBuilderPostFilterPanel.tsx +31 -4
  101. package/src/components/fetch-structure/QueryBuilderResultModifierPanel.tsx +74 -0
  102. package/src/components/fetch-structure/QueryBuilderTDSPanel.tsx +123 -8
  103. package/src/components/fetch-structure/QueryBuilderTDSWindowPanel.tsx +49 -4
  104. package/src/components/filter/QueryBuilderFilterPanel.tsx +31 -4
  105. package/src/components/result/QueryBuilderDataProductErrorPanel.tsx +441 -0
  106. package/src/components/result/QueryBuilderResultPanel.tsx +49 -32
  107. package/src/index.ts +1 -0
  108. package/src/stores/QueryBuilderResultState.ts +107 -19
  109. package/src/stores/QueryBuilderState.ts +239 -29
  110. package/src/stores/QueryBuilder_LegendApplicationPlugin_Extension.ts +20 -0
  111. package/src/stores/QueryLoaderState.ts +47 -6
  112. package/src/stores/data-access/DataProductAccessInfo.ts +37 -0
  113. package/src/stores/data-cube/QueryBuilderDataCubeEngine.ts +1 -1
  114. package/src/stores/explorer/QueryBuilderExplorerState.ts +1 -1
  115. package/src/stores/milestoning/QueryBuilderMilestoningHelper.ts +63 -8
  116. package/src/stores/milestoning/QueryBuilderMilestoningState.ts +20 -0
  117. package/src/stores/workflows/dataProduct/DataProductQueryBuilderState.ts +68 -0
  118. package/tsconfig.json +2 -0
@@ -0,0 +1,441 @@
1
+ /**
2
+ * Copyright (c) 2026-present, Goldman Sachs
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { observer } from 'mobx-react-lite';
18
+ import { useState } from 'react';
19
+ import {
20
+ ChevronDownIcon,
21
+ ChevronRightIcon,
22
+ clsx,
23
+ ExclamationTriangleIcon,
24
+ LockIcon,
25
+ } from '@finos/legend-art';
26
+ import {
27
+ ExecutionError,
28
+ isExecutionEntitlementError,
29
+ isExecutionWarehouseError,
30
+ } from '@finos/legend-graph';
31
+ import type { QueryBuilderState } from '../../stores/QueryBuilderState.js';
32
+ import type { QueryBuilderResultState } from '../../stores/QueryBuilderResultState.js';
33
+ import type { DataProductAccessInfo } from '../../stores/data-access/DataProductAccessInfo.js';
34
+ import type { QueryBuilder_LegendApplicationPlugin_Extension } from '../../stores/QueryBuilder_LegendApplicationPlugin_Extension.js';
35
+ import { QUERY_BUILDER_DOCUMENTATION_KEY } from '../../__lib__/QueryBuilderDocumentation.js';
36
+ import { DataProductQueryBuilderState } from '../../stores/workflows/dataProduct/DataProductQueryBuilderState.js';
37
+
38
+ const buildAccessRequestLink = (
39
+ info: DataProductAccessInfo,
40
+ queryBuilderState: QueryBuilderState,
41
+ ): string | undefined => {
42
+ for (const builder of queryBuilderState.applicationStore.pluginManager
43
+ .getApplicationPlugins()
44
+ .flatMap(
45
+ (plugin) =>
46
+ (
47
+ plugin as QueryBuilder_LegendApplicationPlugin_Extension
48
+ ).getDataProductAccessRequestLinkBuilders?.() ?? [],
49
+ )) {
50
+ const link = builder(info, queryBuilderState);
51
+ if (link) {
52
+ return link;
53
+ }
54
+ }
55
+ return undefined;
56
+ };
57
+
58
+ /**
59
+ * The warehouse FAQ is only offered when the host application has registered it -
60
+ * without it there is nothing actionable to show, and the caller falls back to the
61
+ * default execution error view.
62
+ */
63
+ const getSnowflakeWarehouseFAQUrl = (
64
+ queryBuilderState: QueryBuilderState,
65
+ ): string | undefined =>
66
+ queryBuilderState.applicationStore.documentationService.getDocEntry(
67
+ QUERY_BUILDER_DOCUMENTATION_KEY.SNOWFLAKE_ACTIVE_WAREHOUSE_ISSUE_FAQ,
68
+ )?.url;
69
+
70
+ type DataProductErrorInfoEntry = {
71
+ /**
72
+ * Labels are not unique - a data product can list several support emails - so
73
+ * entries carry their own key.
74
+ */
75
+ key: string;
76
+ label: string;
77
+ value: React.ReactNode;
78
+ };
79
+
80
+ const DataProductErrorPanelLayout = observer(
81
+ (props: {
82
+ icon: React.ReactNode;
83
+ severity: 'error' | 'warn';
84
+ headline: string;
85
+ action: React.ReactNode | undefined;
86
+ entries: DataProductErrorInfoEntry[];
87
+ errorMessage?: string | undefined;
88
+ executionTraceId?: string | undefined;
89
+ errorStackTrace?: string | undefined;
90
+ }) => {
91
+ const {
92
+ icon,
93
+ severity,
94
+ headline,
95
+ action,
96
+ entries,
97
+ errorMessage,
98
+ executionTraceId,
99
+ errorStackTrace,
100
+ } = props;
101
+ const [showStackTrace, setShowStackTrace] = useState(false);
102
+
103
+ return (
104
+ <div className="query-builder__result__data-product-access">
105
+ <div className="query-builder__result__data-product-access__header">
106
+ <div
107
+ className={clsx(
108
+ 'query-builder__result__data-product-access__header__icon',
109
+ `query-builder__result__data-product-access__header__icon--${severity}`,
110
+ )}
111
+ >
112
+ {icon}
113
+ </div>
114
+ <div className="query-builder__result__data-product-access__header__label">
115
+ {headline}
116
+ </div>
117
+ </div>
118
+ {action}
119
+ <div className="query-builder__result__data-product-access__info">
120
+ {entries.map((entry) => (
121
+ <div
122
+ className="query-builder__result__data-product-access__info__entry"
123
+ key={entry.key}
124
+ >
125
+ <div className="query-builder__result__data-product-access__info__entry__label">
126
+ {entry.label}
127
+ </div>
128
+ <div className="query-builder__result__data-product-access__info__entry__value">
129
+ {entry.value}
130
+ </div>
131
+ </div>
132
+ ))}
133
+ </div>
134
+ {errorMessage !== undefined && (
135
+ <div className="query-builder__result__data-product-access__details">
136
+ <div className="query-builder__result__data-product-access__details__header">
137
+ Error details
138
+ </div>
139
+ <div className="query-builder__result__data-product-access__details__message">
140
+ {errorMessage}
141
+ </div>
142
+ {executionTraceId !== undefined && (
143
+ <div className="query-builder__result__data-product-access__details__trace">
144
+ trace: {executionTraceId}
145
+ </div>
146
+ )}
147
+ {errorStackTrace !== undefined && errorStackTrace !== '' && (
148
+ <>
149
+ <button
150
+ className="query-builder__result__data-product-access__details__toggle"
151
+ onClick={() => setShowStackTrace(!showStackTrace)}
152
+ >
153
+ {showStackTrace ? <ChevronDownIcon /> : <ChevronRightIcon />}
154
+ {showStackTrace ? 'Hide stack trace' : 'Show stack trace'}
155
+ </button>
156
+ {showStackTrace && (
157
+ <div className="query-builder__result__data-product-access__details__message">
158
+ {errorStackTrace}
159
+ </div>
160
+ )}
161
+ </>
162
+ )}
163
+ </div>
164
+ )}
165
+ </div>
166
+ );
167
+ },
168
+ );
169
+
170
+ const buildSupportEntries = (
171
+ info: DataProductAccessInfo,
172
+ ): DataProductErrorInfoEntry[] =>
173
+ info.supportEmails.map((email) => ({
174
+ key: `support-${email}`,
175
+ label: 'Support',
176
+ value: (
177
+ <a
178
+ className="query-builder__result__data-product-access__info__entry__value--linkable"
179
+ href={`mailto:${email}`}
180
+ >
181
+ {email}
182
+ </a>
183
+ ),
184
+ }));
185
+
186
+ /**
187
+ * Shown instead of the generic entitlement banner when a query runs against a data
188
+ * product: the raw execution error does not tell the user which data product they are
189
+ * missing access to, nor where to request it.
190
+ */
191
+ export const QueryBuilderDataProductAccessErrorPanel = observer(
192
+ (props: {
193
+ queryBuilderState: QueryBuilderState;
194
+ info: DataProductAccessInfo;
195
+ headline: string;
196
+ errorMessage?: string | undefined;
197
+ executionTraceId?: string | undefined;
198
+ errorStackTrace?: string | undefined;
199
+ }) => {
200
+ const {
201
+ queryBuilderState,
202
+ info,
203
+ headline,
204
+ errorMessage,
205
+ executionTraceId,
206
+ errorStackTrace,
207
+ } = props;
208
+ const accessRequestLink = buildAccessRequestLink(info, queryBuilderState);
209
+
210
+ const requestAccess = (): void => {
211
+ if (accessRequestLink) {
212
+ queryBuilderState.applicationStore.navigationService.navigator.visitAddress(
213
+ accessRequestLink,
214
+ );
215
+ }
216
+ };
217
+
218
+ const entries: DataProductErrorInfoEntry[] = [
219
+ {
220
+ key: 'data-product',
221
+ label: 'Data Product',
222
+ value: info.dataProductLabel,
223
+ },
224
+ ...(info.accessPointGroupLabel !== undefined
225
+ ? [
226
+ {
227
+ key: 'access-point-group',
228
+ label: 'Access Point Group',
229
+ value: info.accessPointGroupLabel,
230
+ },
231
+ ]
232
+ : []),
233
+ ...(info.environment !== undefined
234
+ ? [
235
+ {
236
+ key: 'environment',
237
+ label: 'Environment',
238
+ value: info.environment,
239
+ },
240
+ ]
241
+ : []),
242
+ ...(info.deploymentId !== undefined
243
+ ? [
244
+ {
245
+ key: 'deployment-id',
246
+ label: 'Deployment ID',
247
+ value: info.deploymentId,
248
+ },
249
+ ]
250
+ : []),
251
+ ...(info.warehouse !== undefined
252
+ ? [{ key: 'warehouse', label: 'Warehouse', value: info.warehouse }]
253
+ : []),
254
+ ...buildSupportEntries(info),
255
+ ];
256
+
257
+ return (
258
+ <DataProductErrorPanelLayout
259
+ icon={<LockIcon />}
260
+ severity="error"
261
+ headline={headline}
262
+ action={
263
+ accessRequestLink !== undefined ? (
264
+ <button
265
+ className="btn--dark query-builder__result__data-product-access__action"
266
+ title="Open this data product in Marketplace to request access"
267
+ onClick={requestAccess}
268
+ >
269
+ Request Access in Marketplace
270
+ </button>
271
+ ) : undefined
272
+ }
273
+ entries={entries}
274
+ errorMessage={errorMessage}
275
+ executionTraceId={executionTraceId}
276
+ errorStackTrace={errorStackTrace}
277
+ />
278
+ );
279
+ },
280
+ );
281
+
282
+ /**
283
+ * Warehouse failures are about compute rather than grants on the data, so requesting
284
+ * access to the data product would not resolve them - point the user at the warehouse
285
+ * FAQ instead, and name the warehouse the query actually ran against.
286
+ */
287
+ export const QueryBuilderDataProductWarehouseErrorPanel = observer(
288
+ (props: {
289
+ queryBuilderState: QueryBuilderState;
290
+ info: DataProductAccessInfo;
291
+ faqUrl: string;
292
+ errorMessage?: string | undefined;
293
+ executionTraceId?: string | undefined;
294
+ errorStackTrace?: string | undefined;
295
+ }) => {
296
+ const {
297
+ queryBuilderState,
298
+ info,
299
+ faqUrl,
300
+ errorMessage,
301
+ executionTraceId,
302
+ errorStackTrace,
303
+ } = props;
304
+
305
+ const openFAQ = (): void => {
306
+ queryBuilderState.applicationStore.navigationService.navigator.visitAddress(
307
+ faqUrl,
308
+ );
309
+ };
310
+
311
+ const entries: DataProductErrorInfoEntry[] = [
312
+ ...(info.warehouse !== undefined
313
+ ? [{ key: 'warehouse', label: 'Warehouse', value: info.warehouse }]
314
+ : []),
315
+ ...(info.environment !== undefined
316
+ ? [
317
+ {
318
+ key: 'environment',
319
+ label: 'Environment',
320
+ value: info.environment,
321
+ },
322
+ ]
323
+ : []),
324
+ {
325
+ key: 'data-product',
326
+ label: 'Data Product',
327
+ value: info.dataProductLabel,
328
+ },
329
+ ...buildSupportEntries(info),
330
+ ];
331
+
332
+ return (
333
+ <DataProductErrorPanelLayout
334
+ icon={<ExclamationTriangleIcon />}
335
+ severity="warn"
336
+ headline={
337
+ info.warehouse !== undefined
338
+ ? `Can't access warehouse ${info.warehouse}`
339
+ : `Can't access the compute warehouse`
340
+ }
341
+ action={
342
+ <button
343
+ className="btn--dark query-builder__result__data-product-access__action"
344
+ title="Open the Snowflake warehouse FAQ"
345
+ onClick={openFAQ}
346
+ >
347
+ Snowflake Warehouse FAQ
348
+ </button>
349
+ }
350
+ entries={entries}
351
+ errorMessage={errorMessage}
352
+ executionTraceId={executionTraceId}
353
+ errorStackTrace={errorStackTrace}
354
+ />
355
+ );
356
+ },
357
+ );
358
+
359
+ const getDataProductAccessInfo = (
360
+ queryBuilderState: QueryBuilderState,
361
+ ): DataProductAccessInfo | undefined =>
362
+ queryBuilderState instanceof DataProductQueryBuilderState
363
+ ? queryBuilderState.dataProductAccessInfo
364
+ : undefined;
365
+
366
+ /**
367
+ * Builds the view to show in place of the default execution error panel when a query
368
+ * runs against a data product, or `undefined` when the caller should fall back to that
369
+ * default view (not a data product, an error we do not recognize, or a warehouse
370
+ * failure with no FAQ registered to point at).
371
+ */
372
+ export const buildDataProductExecutionErrorPanel = (
373
+ resultState: QueryBuilderResultState,
374
+ executionError: string | Error,
375
+ ): React.ReactNode | undefined => {
376
+ const queryBuilderState = resultState.queryBuilderState;
377
+ const info = getDataProductAccessInfo(queryBuilderState);
378
+ if (!info) {
379
+ return undefined;
380
+ }
381
+ // NOTE: classify on the notification service's message rather than `error.message`:
382
+ // for an `ApplicationError` it returns the detail, which also covers the stack trace
383
+ const classifiableErrorMessage =
384
+ queryBuilderState.applicationStore.notificationService.getErrorMessage(
385
+ executionError,
386
+ );
387
+ const errorMessage =
388
+ executionError instanceof Error ? executionError.message : executionError;
389
+ const errorStackTrace =
390
+ executionError instanceof ExecutionError ? executionError.stack : '';
391
+
392
+ if (isExecutionWarehouseError(classifiableErrorMessage)) {
393
+ const faqUrl = getSnowflakeWarehouseFAQUrl(queryBuilderState);
394
+ return faqUrl !== undefined ? (
395
+ <QueryBuilderDataProductWarehouseErrorPanel
396
+ queryBuilderState={queryBuilderState}
397
+ info={info}
398
+ faqUrl={faqUrl}
399
+ errorMessage={errorMessage}
400
+ executionTraceId={resultState.executionTraceId}
401
+ errorStackTrace={errorStackTrace}
402
+ />
403
+ ) : undefined;
404
+ }
405
+
406
+ if (isExecutionEntitlementError(classifiableErrorMessage)) {
407
+ return (
408
+ <QueryBuilderDataProductAccessErrorPanel
409
+ queryBuilderState={queryBuilderState}
410
+ info={info}
411
+ headline={`You don't have access to ${
412
+ info.accessPointGroupLabel
413
+ ? `${info.accessPointGroupLabel} in ${info.dataProductLabel}`
414
+ : info.dataProductLabel
415
+ }`}
416
+ errorMessage={errorMessage}
417
+ executionTraceId={resultState.executionTraceId}
418
+ errorStackTrace={errorStackTrace}
419
+ />
420
+ );
421
+ }
422
+
423
+ return undefined;
424
+ };
425
+
426
+ /**
427
+ * A row access policy filters rows out silently rather than failing the query, so an
428
+ * empty result is also a plausible entitlement problem for data products.
429
+ */
430
+ export const buildDataProductEmptyResultPanel = (
431
+ queryBuilderState: QueryBuilderState,
432
+ ): React.ReactNode | undefined => {
433
+ const info = getDataProductAccessInfo(queryBuilderState);
434
+ return info ? (
435
+ <QueryBuilderDataProductAccessErrorPanel
436
+ queryBuilderState={queryBuilderState}
437
+ info={info}
438
+ headline="Query returned no data - this may be a row-level access policy"
439
+ />
440
+ ) : undefined;
441
+ };
@@ -56,6 +56,7 @@ import {
56
56
  TDSExecutionResult,
57
57
  RawExecutionResult,
58
58
  ExecutionError,
59
+ isExecutionAccessError,
59
60
  } from '@finos/legend-graph';
60
61
  import {
61
62
  ActionAlertActionType,
@@ -81,12 +82,10 @@ import type { QueryBuilder_LegendApplicationPlugin_Extension } from '../../store
81
82
  import type { QueryBuilderResultState } from '../../stores/QueryBuilderResultState.js';
82
83
  import { QueryBuilderBaseInfoTooltip } from '../shared/QueryBuilderPropertyInfoTooltip.js';
83
84
  import { LineageViewer } from '../lineage/LineageViewer.js';
84
-
85
- const PERMISSION_ERRORS = [
86
- 'permission denied',
87
- 'invalid user id or password',
88
- 'Incorrect username or password',
89
- ];
85
+ import {
86
+ buildDataProductEmptyResultPanel,
87
+ buildDataProductExecutionErrorPanel,
88
+ } from './QueryBuilderDataProductErrorPanel.js';
90
89
 
91
90
  export const QueryBuilderExecutionErrorPanel = observer(
92
91
  (props: {
@@ -99,13 +98,12 @@ export const QueryBuilderExecutionErrorPanel = observer(
99
98
  executionError instanceof Error ? executionError.message : executionError;
100
99
  const errorStackTrace =
101
100
  executionError instanceof ExecutionError ? executionError.stack : '';
102
- const isPermissionDeniedError = Boolean(
103
- PERMISSION_ERRORS.find((e) =>
104
- queryBuilderState.applicationStore.notificationService
105
- .getErrorMessage(executionError)
106
- ?.toLowerCase()
107
- .includes(e),
108
- ),
101
+ const classifiableErrorMessage =
102
+ queryBuilderState.applicationStore.notificationService.getErrorMessage(
103
+ executionError,
104
+ );
105
+ const canCheckEntitlements = isExecutionAccessError(
106
+ classifiableErrorMessage,
109
107
  );
110
108
  const openCheckEntitlmentsEditor = (): void => {
111
109
  queryBuilderState.checkEntitlementsState.setShowCheckEntitlementsViewer(
@@ -124,9 +122,19 @@ export const QueryBuilderExecutionErrorPanel = observer(
124
122
  }
125
123
  };
126
124
 
125
+ // data products get their own views: the raw error names neither the data product
126
+ // access is missing on, nor where to go about it
127
+ const dataProductErrorPanel = buildDataProductExecutionErrorPanel(
128
+ resultState,
129
+ executionError,
130
+ );
131
+ if (dataProductErrorPanel) {
132
+ return <>{dataProductErrorPanel}</>;
133
+ }
134
+
127
135
  return (
128
136
  <>
129
- {isPermissionDeniedError && (
137
+ {canCheckEntitlements && (
130
138
  <div className="query-builder__result__permission-error">
131
139
  <div className="query-builder__result__permission-error__header">
132
140
  Entitlement / Authorization error - Please
@@ -150,7 +158,7 @@ export const QueryBuilderExecutionErrorPanel = observer(
150
158
  <div
151
159
  className={clsx('query-builder__result__execution-error', {
152
160
  'query-builder__result__execution-error--max':
153
- !isPermissionDeniedError,
161
+ !canCheckEntitlements,
154
162
  })}
155
163
  >
156
164
  <div className="query-builder__result__execution-error__header">
@@ -211,6 +219,11 @@ export const QueryBuilderEmptyExecutionResultPanel = observer(
211
219
  true,
212
220
  );
213
221
  };
222
+ const dataProductEmptyResultPanel =
223
+ buildDataProductEmptyResultPanel(queryBuilderState);
224
+ if (dataProductEmptyResultPanel) {
225
+ return <>{dataProductEmptyResultPanel}</>;
226
+ }
214
227
 
215
228
  return (
216
229
  <div className="query-builder__result__empty-result-warning">
@@ -514,23 +527,27 @@ export const QueryBuilderResultPanel = observer(
514
527
  plugin as QueryBuilder_LegendApplicationPlugin_Extension
515
528
  ).getExtraQueryBuilderExportMenuActionConfigurations?.() ?? [],
516
529
  )
517
- .map((item) => (
518
- <MenuContentItem
519
- key={item.key}
520
- title={
521
- !item.disableFunc?.(queryBuilderState)
522
- ? item.title
523
- : (item.disableMessage ?? 'Unsupported')
524
- }
525
- disabled={item.disableFunc?.(queryBuilderState) ?? false}
526
- onClick={() => {
527
- item.onClick(queryBuilderState);
528
- }}
529
- >
530
- {item.icon && <MenuContentItemIcon>{item.icon}</MenuContentItemIcon>}
531
- <MenuContentItemLabel>{item.label}</MenuContentItemLabel>
532
- </MenuContentItem>
533
- ));
530
+ .map((item) => {
531
+ const isDisabled = item.disableFunc?.(queryBuilderState) ?? false;
532
+ const disableMessage =
533
+ item.getDisableMessage?.(queryBuilderState) ?? item.disableMessage;
534
+
535
+ return (
536
+ <MenuContentItem
537
+ key={item.key}
538
+ title={!isDisabled ? item.title : (disableMessage ?? 'Unsupported')}
539
+ disabled={isDisabled}
540
+ onClick={() => {
541
+ item.onClick(queryBuilderState);
542
+ }}
543
+ >
544
+ {item.icon && (
545
+ <MenuContentItemIcon>{item.icon}</MenuContentItemIcon>
546
+ )}
547
+ <MenuContentItemLabel>{item.label}</MenuContentItemLabel>
548
+ </MenuContentItem>
549
+ );
550
+ });
534
551
 
535
552
  const isLoading =
536
553
  resultState.isRunningQuery || resultState.isGeneratingPlan;
package/src/index.ts CHANGED
@@ -160,6 +160,7 @@ export * from './stores/QueryLoaderState.js';
160
160
  export * from './stores/QueryBuilder_LegendApplicationPlugin_Extension.js';
161
161
 
162
162
  export * from './stores/data-access/DataAccessState.js';
163
+ export * from './stores/data-access/DataProductAccessInfo.js';
163
164
  export * from './components/data-access/DataAccessOverview.js';
164
165
  export * from './stores/query-workflow/QueryBuilderWorkFlowState.js';
165
166
  export * from './stores/explorer/QueryBuilderExplorerState.js';