@finos/legend-query-builder 4.14.19 → 4.14.20

Sign up to get free protection for your applications and to get access to all the features.
@@ -41,11 +41,12 @@ import {
41
41
  ThinChevronRightIcon,
42
42
  InfoCircleIcon,
43
43
  } from '@finos/legend-art';
44
- import type { LightQuery } from '@finos/legend-graph';
44
+ import type { LightQuery, RawLambda } from '@finos/legend-graph';
45
45
  import {
46
46
  debounce,
47
47
  formatDistanceToNow,
48
48
  guaranteeNonNullable,
49
+ isNonNullable,
49
50
  quantifyList,
50
51
  } from '@finos/legend-shared';
51
52
  import { flowResult } from 'mobx';
@@ -110,6 +111,21 @@ export const QueryLoader = observer(
110
111
  const searchInputRef = useRef<HTMLInputElement>(null);
111
112
  const queryRenameInputRef = useRef<HTMLInputElement>(null);
112
113
  const results = queryLoaderState.queries;
114
+ const curatedTemplateQueries =
115
+ queryLoaderState.curatedTemplateQuerySepcifications
116
+ .map((s) =>
117
+ queryLoaderState.queryBuilderState
118
+ ? s.getCuratedTemplateQueries(queryLoaderState.queryBuilderState)
119
+ : [],
120
+ )
121
+ .flat();
122
+ const loadCuratedTemplateQuery =
123
+ queryLoaderState.curatedTemplateQuerySepcifications
124
+ // already using an arrow function suggested by @typescript-eslint/unbound-method
125
+ // eslint-disable-next-line
126
+ .map((s) => () => s.loadCuratedTemplateQuery)
127
+ .filter(isNonNullable)[0];
128
+
113
129
  const [isMineOnly, setIsMineOnly] = useState(false);
114
130
  const [showQueryNameEditInput, setShowQueryNameEditInput] = useState<
115
131
  number | undefined
@@ -173,7 +189,25 @@ export const QueryLoader = observer(
173
189
  debouncedLoadQueries.cancel();
174
190
  debouncedLoadQueries(queryLoaderState.searchText);
175
191
  };
176
-
192
+ const toggleCuratedTemplate = (): void => {
193
+ Array.from(queryLoaderState.extraFilters).map(([key, value]) =>
194
+ queryLoaderState.extraFilters.set(key, false),
195
+ );
196
+ queryLoaderState.setShowCurrentUserQueriesOnly(false);
197
+ setIsMineOnly(false);
198
+ queryLoaderState.extraQueryFilterOptionsRelatedToTemplateQuery.forEach(
199
+ (op) =>
200
+ queryLoaderState.extraFilters.set(
201
+ op,
202
+ !queryLoaderState.isCuratedTemplateToggled,
203
+ ),
204
+ );
205
+ queryLoaderState.showingDefaultQueries =
206
+ queryLoaderState.isCuratedTemplateToggled;
207
+ queryLoaderState.setIsCuratedTemplateToggled(
208
+ !queryLoaderState.isCuratedTemplateToggled,
209
+ );
210
+ };
177
211
  useEffect(() => {
178
212
  flowResult(queryLoaderState.searchQueries('')).catch(
179
213
  applicationStore.alertUnhandledError,
@@ -207,10 +241,16 @@ export const QueryLoader = observer(
207
241
  }
208
242
  };
209
243
 
210
- const showPreview = (queryId: string): void => {
211
- flowResult(queryLoaderState.getPreviewQueryContent(queryId)).catch(
212
- applicationStore.alertUnhandledError,
213
- );
244
+ const showPreview = (
245
+ queryId: string | undefined,
246
+ template?: {
247
+ queryName: string;
248
+ queryContent: RawLambda;
249
+ },
250
+ ): void => {
251
+ flowResult(
252
+ queryLoaderState.getPreviewQueryContent(queryId, template),
253
+ ).catch(applicationStore.alertUnhandledError);
214
254
  queryLoaderState.setShowPreviewViewer(true);
215
255
  };
216
256
 
@@ -254,6 +294,12 @@ export const QueryLoader = observer(
254
294
  'query-loader__filter__toggler__btn--toggled': isMineOnly,
255
295
  })}
256
296
  onClick={toggleShowCurrentUserQueriesOnly}
297
+ disabled={queryLoaderState.isCuratedTemplateToggled}
298
+ title={
299
+ queryLoaderState.isCuratedTemplateToggled
300
+ ? 'current fitler is disabled when `Curated Template Query` is on'
301
+ : 'click to add filter'
302
+ }
257
303
  tabIndex={-1}
258
304
  >
259
305
  Mine Only
@@ -267,6 +313,12 @@ export const QueryLoader = observer(
267
313
  className={clsx('query-loader__filter__toggler__btn', {
268
314
  'query-loader__filter__toggler__btn--toggled': value,
269
315
  })}
316
+ disabled={queryLoaderState.isCuratedTemplateToggled}
317
+ title={
318
+ queryLoaderState.isCuratedTemplateToggled
319
+ ? 'current fitler is disabled when `Curated Template Query` is on'
320
+ : 'click to add filter'
321
+ }
270
322
  onClick={(): void => toggleExtraFilters(key)}
271
323
  tabIndex={-1}
272
324
  >
@@ -276,6 +328,21 @@ export const QueryLoader = observer(
276
328
  )}
277
329
  </div>
278
330
  )}
331
+ {queryLoaderState.extraQueryFilterOptionsRelatedToTemplateQuery
332
+ .length > 0 && (
333
+ <div className="query-loader__filter__extra__filters">
334
+ <button
335
+ className={clsx('query-loader__filter__toggler__btn', {
336
+ 'query-loader__filter__toggler__btn--toggled':
337
+ queryLoaderState.isCuratedTemplateToggled,
338
+ })}
339
+ onClick={toggleCuratedTemplate}
340
+ tabIndex={-1}
341
+ >
342
+ Curated Template Query
343
+ </button>
344
+ </div>
345
+ )}
279
346
  </div>
280
347
  </div>
281
348
  </div>
@@ -297,7 +364,20 @@ export const QueryLoader = observer(
297
364
  queryLoaderState.generateDefaultQueriesSummaryText?.(
298
365
  results,
299
366
  ) ?? 'Refine your search to get better matches'
300
- ) : results.length >= QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT ? (
367
+ ) : !queryLoaderState.isCuratedTemplateToggled ? (
368
+ results.length >= QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT ? (
369
+ <>
370
+ {`Found ${QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT}+ matches`}{' '}
371
+ <InfoCircleIcon
372
+ title="Some queries are not listed, refine your search to get better matches"
373
+ className="query-loader__results__summary__info"
374
+ />
375
+ </>
376
+ ) : (
377
+ `Found ${quantifyList(results, 'match', 'matches')}`
378
+ )
379
+ ) : curatedTemplateQueries.length >=
380
+ QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT ? (
301
381
  <>
302
382
  {`Found ${QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT}+ matches`}{' '}
303
383
  <InfoCircleIcon
@@ -306,139 +386,211 @@ export const QueryLoader = observer(
306
386
  />
307
387
  </>
308
388
  ) : (
309
- `Found ${quantifyList(results, 'match', 'matches')}`
389
+ `Found ${quantifyList(
390
+ curatedTemplateQueries,
391
+ 'match',
392
+ 'matches',
393
+ )}`
310
394
  )}
311
395
  </div>
312
- {results
313
- .slice(0, QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT)
314
- .map((query, idx) => (
315
- <div
316
- className="query-loader__result"
317
- title={`Click to ${loadActionLabel}...`}
318
- key={query.id}
319
- onClick={() => queryLoaderState.loadQuery(query)}
320
- >
321
- <div className="query-loader__result__content">
322
- {showQueryNameEditInput === idx ? (
323
- <div className="query-loader__result__title__editor">
324
- <input
325
- className="query-loader__result__title__editor__input input--dark"
326
- spellCheck={false}
327
- ref={queryRenameInputRef}
328
- value={queryNameInputValue}
329
- onChange={changeQueryNameInputValue}
330
- onKeyDown={(event) => {
331
- if (event.code === 'Enter') {
332
- event.stopPropagation();
333
- renameQuery(query)();
334
- } else if (event.code === 'Escape') {
335
- event.stopPropagation();
336
- hideEditQueryNameInput();
337
- }
338
- }}
339
- onBlur={() => hideEditQueryNameInput()}
340
- // avoid clicking on the input causing the call to load query
341
- onClick={(event) => event.stopPropagation()}
342
- />
396
+ {!queryLoaderState.isCuratedTemplateToggled &&
397
+ results
398
+ .slice(0, QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT)
399
+ .map((query, idx) => (
400
+ <div
401
+ className="query-loader__result"
402
+ title={`Click to ${loadActionLabel}...`}
403
+ key={query.id}
404
+ onClick={() => queryLoaderState.loadQuery(query)}
405
+ >
406
+ <div className="query-loader__result__content">
407
+ {showQueryNameEditInput === idx ? (
408
+ <div className="query-loader__result__title__editor">
409
+ <input
410
+ className="query-loader__result__title__editor__input input--dark"
411
+ spellCheck={false}
412
+ ref={queryRenameInputRef}
413
+ value={queryNameInputValue}
414
+ onChange={changeQueryNameInputValue}
415
+ onKeyDown={(event) => {
416
+ if (event.code === 'Enter') {
417
+ event.stopPropagation();
418
+ renameQuery(query)();
419
+ } else if (event.code === 'Escape') {
420
+ event.stopPropagation();
421
+ hideEditQueryNameInput();
422
+ }
423
+ }}
424
+ onBlur={() => hideEditQueryNameInput()}
425
+ // avoid clicking on the input causing the call to load query
426
+ onClick={(event) => event.stopPropagation()}
427
+ />
428
+ </div>
429
+ ) : (
430
+ <div
431
+ className="query-loader__result__title"
432
+ title={query.name}
433
+ >
434
+ {query.name}
435
+ </div>
436
+ )}
437
+ <div className="query-loader__result__description">
438
+ <div className="query-loader__result__description__date__icon">
439
+ <LastModifiedIcon />
440
+ </div>
441
+ <div className="query-loader__result__description__date">
442
+ {query.lastUpdatedAt
443
+ ? formatDistanceToNow(
444
+ new Date(query.lastUpdatedAt),
445
+ {
446
+ includeSeconds: true,
447
+ addSuffix: true,
448
+ },
449
+ )
450
+ : '(unknown)'}
451
+ </div>
452
+ <div
453
+ className={clsx(
454
+ 'query-loader__result__description__author__icon',
455
+ {
456
+ 'query-loader__result__description__author__icon--owner':
457
+ query.isCurrentUserQuery,
458
+ },
459
+ )}
460
+ >
461
+ <UserIcon />
462
+ </div>
463
+ <div className="query-loader__result__description__author__name">
464
+ {query.isCurrentUserQuery ? (
465
+ <div
466
+ title={query.owner}
467
+ className="query-loader__result__description__owner"
468
+ >
469
+ Me
470
+ </div>
471
+ ) : (
472
+ query.owner
473
+ )}
474
+ </div>
343
475
  </div>
344
- ) : (
476
+ </div>
477
+ <DropdownMenu
478
+ className="query-loader__result__actions-menu"
479
+ title="More Actions..."
480
+ content={
481
+ <MenuContent>
482
+ <MenuContentItem
483
+ onClick={(): void => showPreview(query.id)}
484
+ >
485
+ Show Query Preview
486
+ </MenuContentItem>
487
+ {!queryLoaderState.isReadOnly && (
488
+ <MenuContentItem
489
+ disabled={!query.isCurrentUserQuery}
490
+ onClick={deleteQuery(query)}
491
+ >
492
+ Delete
493
+ </MenuContentItem>
494
+ )}
495
+ {!queryLoaderState.isReadOnly && (
496
+ <MenuContentItem
497
+ disabled={!query.isCurrentUserQuery}
498
+ onClick={showEditQueryNameInput(
499
+ query.name,
500
+ idx,
501
+ )}
502
+ >
503
+ Rename
504
+ </MenuContentItem>
505
+ )}
506
+ </MenuContent>
507
+ }
508
+ menuProps={{
509
+ anchorOrigin: {
510
+ vertical: 'bottom',
511
+ horizontal: 'left',
512
+ },
513
+ transformOrigin: {
514
+ vertical: 'top',
515
+ horizontal: 'left',
516
+ },
517
+ elevation: 7,
518
+ }}
519
+ >
520
+ <MoreVerticalIcon />
521
+ </DropdownMenu>
522
+ <div className="query-loader__result__arrow">
523
+ <ThinChevronRightIcon />
524
+ </div>
525
+ </div>
526
+ ))}
527
+ {queryLoaderState.queryBuilderState &&
528
+ queryLoaderState.isCuratedTemplateToggled &&
529
+ loadCuratedTemplateQuery &&
530
+ curatedTemplateQueries
531
+ .slice(0, QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT)
532
+ .map((templateQuery, idx) => (
533
+ <div
534
+ className="query-loader__result"
535
+ title={`Click to ${loadActionLabel}...`}
536
+ key={templateQuery.title}
537
+ onClick={() => {
538
+ loadCuratedTemplateQuery()(
539
+ templateQuery,
540
+ guaranteeNonNullable(
541
+ queryLoaderState.queryBuilderState,
542
+ ),
543
+ );
544
+ queryLoaderState.setQueryLoaderDialogOpen(false);
545
+ }}
546
+ >
547
+ <div className="query-loader__result__content">
345
548
  <div
346
549
  className="query-loader__result__title"
347
- title={query.name}
348
- >
349
- {query.name}
350
- </div>
351
- )}
352
- <div className="query-loader__result__description">
353
- <div className="query-loader__result__description__date__icon">
354
- <LastModifiedIcon />
355
- </div>
356
- <div className="query-loader__result__description__date">
357
- {query.lastUpdatedAt
358
- ? formatDistanceToNow(
359
- new Date(query.lastUpdatedAt),
360
- {
361
- includeSeconds: true,
362
- addSuffix: true,
363
- },
364
- )
365
- : '(unknown)'}
366
- </div>
367
- <div
368
- className={clsx(
369
- 'query-loader__result__description__author__icon',
370
- {
371
- 'query-loader__result__description__author__icon--owner':
372
- query.isCurrentUserQuery,
373
- },
374
- )}
550
+ title={templateQuery.title}
375
551
  >
376
- <UserIcon />
552
+ {templateQuery.title}
377
553
  </div>
378
- <div className="query-loader__result__description__author__name">
379
- {query.isCurrentUserQuery ? (
380
- <div
381
- title={query.owner}
382
- className="query-loader__result__description__owner"
383
- >
384
- Me
385
- </div>
386
- ) : (
387
- query.owner
388
- )}
554
+ <div className="query-loader__result__description">
555
+ {templateQuery.description}
389
556
  </div>
390
557
  </div>
391
- </div>
392
- <DropdownMenu
393
- className="query-loader__result__actions-menu"
394
- title="More Actions..."
395
- content={
396
- <MenuContent>
397
- <MenuContentItem
398
- onClick={(): void => showPreview(query.id)}
399
- >
400
- Show Query Preview
401
- </MenuContentItem>
402
- {!queryLoaderState.isReadOnly && (
403
- <MenuContentItem
404
- disabled={!query.isCurrentUserQuery}
405
- onClick={deleteQuery(query)}
406
- >
407
- Delete
408
- </MenuContentItem>
409
- )}
410
- {!queryLoaderState.isReadOnly && (
558
+ <DropdownMenu
559
+ className="query-loader__result__actions-menu"
560
+ title="More Actions..."
561
+ content={
562
+ <MenuContent>
411
563
  <MenuContentItem
412
- disabled={!query.isCurrentUserQuery}
413
- onClick={showEditQueryNameInput(
414
- query.name,
415
- idx,
416
- )}
564
+ onClick={(): void =>
565
+ showPreview(undefined, {
566
+ queryContent: templateQuery.query,
567
+ queryName: templateQuery.title,
568
+ })
569
+ }
417
570
  >
418
- Rename
571
+ Show Query Preview
419
572
  </MenuContentItem>
420
- )}
421
- </MenuContent>
422
- }
423
- menuProps={{
424
- anchorOrigin: {
425
- vertical: 'bottom',
426
- horizontal: 'left',
427
- },
428
- transformOrigin: {
429
- vertical: 'top',
430
- horizontal: 'left',
431
- },
432
- elevation: 7,
433
- }}
434
- >
435
- <MoreVerticalIcon />
436
- </DropdownMenu>
437
- <div className="query-loader__result__arrow">
438
- <ThinChevronRightIcon />
573
+ </MenuContent>
574
+ }
575
+ menuProps={{
576
+ anchorOrigin: {
577
+ vertical: 'bottom',
578
+ horizontal: 'left',
579
+ },
580
+ transformOrigin: {
581
+ vertical: 'top',
582
+ horizontal: 'left',
583
+ },
584
+ elevation: 7,
585
+ }}
586
+ >
587
+ <MoreVerticalIcon />
588
+ </DropdownMenu>
589
+ <div className="query-loader__result__arrow">
590
+ <ThinChevronRightIcon />
591
+ </div>
439
592
  </div>
440
- </div>
441
- ))}
593
+ ))}
442
594
  </>
443
595
  )}
444
596
  {!queryLoaderState.searchQueriesState.hasCompleted && (
@@ -16,12 +16,29 @@
16
16
 
17
17
  import type { LegendApplicationPlugin } from '@finos/legend-application';
18
18
  import type { QueryBuilderState } from './QueryBuilderState.js';
19
- import type { QuerySearchSpecification } from '@finos/legend-graph';
19
+ import type { QuerySearchSpecification, RawLambda } from '@finos/legend-graph';
20
20
  import type {
21
21
  DataAccessState,
22
22
  DatasetAccessInfo,
23
23
  } from './data-access/DataAccessState.js';
24
24
 
25
+ export type CuratedTemplateQuery = {
26
+ title: string;
27
+ description: string | undefined;
28
+ query: RawLambda;
29
+ executionContextKey: string;
30
+ };
31
+
32
+ export type CuratedTemplateQuerySpecification = {
33
+ getCuratedTemplateQueries(
34
+ queryBuilderState: QueryBuilderState,
35
+ ): CuratedTemplateQuery[];
36
+ loadCuratedTemplateQuery(
37
+ templateQuery: CuratedTemplateQuery,
38
+ queryBuilderState: QueryBuilderState,
39
+ ): void;
40
+ };
41
+
25
42
  export type LoadQueryFilterOption = {
26
43
  key: string;
27
44
  label: (queryBuilderState: QueryBuilderState) => string | undefined;
@@ -59,11 +76,23 @@ export type TemplateQueryPanelContentRenderer = (
59
76
 
60
77
  export interface QueryBuilder_LegendApplicationPlugin_Extension
61
78
  extends LegendApplicationPlugin {
79
+ /**
80
+ * Get the list of template query specifications
81
+ */
82
+ getCuratedTemplateQuerySpecifications?(): CuratedTemplateQuerySpecification[];
83
+
62
84
  /**
63
85
  * Get the list of filter options for query loader.
64
86
  */
65
87
  getExtraLoadQueryFilterOptions?(): LoadQueryFilterOption[];
66
88
 
89
+ /**
90
+ * Get the list of filter options related to template query
91
+ */
92
+ getQueryFilterOptionsRelatedToTemplateQuery?(): (
93
+ queryBuilderState: QueryBuilderState,
94
+ ) => string[];
95
+
67
96
  /**
68
97
  * Get the list of warehouse entitlement configurations
69
98
  */