@finos/legend-query-builder 3.1.0 → 3.2.0

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 (39) hide show
  1. package/lib/__lib__/QueryBuilderTelemetryHelper.d.ts +20 -20
  2. package/lib/__lib__/QueryBuilderTelemetryHelper.d.ts.map +1 -1
  3. package/lib/__lib__/QueryBuilderTelemetryHelper.js +40 -40
  4. package/lib/__lib__/QueryBuilderTelemetryHelper.js.map +1 -1
  5. package/lib/components/QueryLoader.d.ts +30 -0
  6. package/lib/components/QueryLoader.d.ts.map +1 -0
  7. package/lib/components/QueryLoader.js +160 -0
  8. package/lib/components/QueryLoader.js.map +1 -0
  9. package/lib/components/shared/QueryBuilderPanelIssueCountBadge.d.ts.map +1 -1
  10. package/lib/components/shared/QueryBuilderPanelIssueCountBadge.js +2 -1
  11. package/lib/components/shared/QueryBuilderPanelIssueCountBadge.js.map +1 -1
  12. package/lib/index.css +2 -2
  13. package/lib/index.css.map +1 -1
  14. package/lib/index.d.ts +3 -0
  15. package/lib/index.d.ts.map +1 -1
  16. package/lib/index.js +3 -0
  17. package/lib/index.js.map +1 -1
  18. package/lib/package.json +1 -1
  19. package/lib/stores/QueryBuilder_LegendApplicationPlugin_Extension.d.ts +30 -0
  20. package/lib/stores/QueryBuilder_LegendApplicationPlugin_Extension.d.ts.map +1 -0
  21. package/lib/stores/QueryBuilder_LegendApplicationPlugin_Extension.js +17 -0
  22. package/lib/stores/QueryBuilder_LegendApplicationPlugin_Extension.js.map +1 -0
  23. package/lib/stores/QueryLoaderState.d.ts +67 -0
  24. package/lib/stores/QueryLoaderState.d.ts.map +1 -0
  25. package/lib/stores/QueryLoaderState.js +205 -0
  26. package/lib/stores/QueryLoaderState.js.map +1 -0
  27. package/lib/stores/shared/ValueSpecificationEditorHelper.d.ts +3 -1
  28. package/lib/stores/shared/ValueSpecificationEditorHelper.d.ts.map +1 -1
  29. package/lib/stores/shared/ValueSpecificationEditorHelper.js +7 -1
  30. package/lib/stores/shared/ValueSpecificationEditorHelper.js.map +1 -1
  31. package/package.json +8 -8
  32. package/src/__lib__/QueryBuilderTelemetryHelper.ts +40 -59
  33. package/src/components/QueryLoader.tsx +501 -0
  34. package/src/components/shared/QueryBuilderPanelIssueCountBadge.tsx +2 -1
  35. package/src/index.ts +3 -0
  36. package/src/stores/QueryBuilder_LegendApplicationPlugin_Extension.ts +36 -0
  37. package/src/stores/QueryLoaderState.ts +298 -0
  38. package/src/stores/shared/ValueSpecificationEditorHelper.ts +39 -2
  39. package/tsconfig.json +3 -0
@@ -0,0 +1,501 @@
1
+ /**
2
+ * Copyright (c) 2020-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 { useApplicationStore } from '@finos/legend-application';
18
+ import {
19
+ CODE_EDITOR_LANGUAGE,
20
+ CodeEditor,
21
+ } from '@finos/legend-lego/code-editor';
22
+ import {
23
+ Dialog,
24
+ Modal,
25
+ ModalTitle,
26
+ clsx,
27
+ SearchIcon,
28
+ TimesIcon,
29
+ DropdownMenu,
30
+ MenuContent,
31
+ MenuContentItem,
32
+ BlankPanelContent,
33
+ PanelLoadingIndicator,
34
+ ModalHeader,
35
+ ModalBody,
36
+ ModalFooter,
37
+ ModalFooterButton,
38
+ UserIcon,
39
+ LastModifiedIcon,
40
+ MoreVerticalIcon,
41
+ ThinChevronRightIcon,
42
+ InfoCircleIcon,
43
+ } from '@finos/legend-art';
44
+ import type { LightQuery } from '@finos/legend-graph';
45
+ import {
46
+ debounce,
47
+ formatDistanceToNow,
48
+ guaranteeNonNullable,
49
+ quantifyList,
50
+ } from '@finos/legend-shared';
51
+ import { flowResult } from 'mobx';
52
+ import { observer } from 'mobx-react-lite';
53
+ import { useRef, useState, useMemo, useEffect } from 'react';
54
+ import {
55
+ QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT,
56
+ type QueryLoaderState,
57
+ } from '../stores/QueryLoaderState.js';
58
+
59
+ const QueryPreviewViewer = observer(
60
+ (props: { queryLoaderState: QueryLoaderState }) => {
61
+ const { queryLoaderState } = props;
62
+ const close = (): void => {
63
+ queryLoaderState.setShowPreviewViewer(false);
64
+ };
65
+ return (
66
+ <Dialog
67
+ open={queryLoaderState.showPreviewViewer}
68
+ onClose={close}
69
+ classes={{
70
+ root: 'editor-modal__root-container',
71
+ container: 'editor-modal__container',
72
+ paper: 'editor-modal__content',
73
+ }}
74
+ >
75
+ <Modal className="editor-modal" darkMode={true}>
76
+ <ModalHeader
77
+ title={
78
+ guaranteeNonNullable(queryLoaderState.queryPreviewContent).name
79
+ }
80
+ />
81
+ <ModalBody>
82
+ <CodeEditor
83
+ inputValue={
84
+ guaranteeNonNullable(queryLoaderState.queryPreviewContent)
85
+ .content
86
+ }
87
+ isReadOnly={true}
88
+ language={CODE_EDITOR_LANGUAGE.PURE}
89
+ showMiniMap={true}
90
+ />
91
+ </ModalBody>
92
+ <ModalFooter>
93
+ <ModalFooterButton onClick={close} text="Close" />
94
+ </ModalFooter>
95
+ </Modal>
96
+ </Dialog>
97
+ );
98
+ },
99
+ );
100
+
101
+ export const QueryLoader = observer(
102
+ (props: { queryLoaderState: QueryLoaderState; loadActionLabel: string }) => {
103
+ const { queryLoaderState, loadActionLabel } = props;
104
+ const applicationStore = useApplicationStore();
105
+ const searchInputRef = useRef<HTMLInputElement>(null);
106
+ const queryRenameInputRef = useRef<HTMLInputElement>(null);
107
+ const results = queryLoaderState.queries;
108
+ const [isMineOnly, setIsMineOnly] = useState(false);
109
+ const [showQueryNameEditInput, setShowQueryNameEditInput] = useState<
110
+ number | undefined
111
+ >();
112
+ useEffect(() => {
113
+ queryRenameInputRef.current?.focus();
114
+ queryRenameInputRef.current?.select();
115
+ }, [showQueryNameEditInput]);
116
+ const [queryNameInputValue, setQueryNameInputValue] = useState<string>('');
117
+ const showEditQueryNameInput =
118
+ (value: string, idx: number): (() => void) =>
119
+ (): void => {
120
+ setQueryNameInputValue(value);
121
+ setShowQueryNameEditInput(idx);
122
+ };
123
+ const hideEditQueryNameInput = (): void => {
124
+ setShowQueryNameEditInput(undefined);
125
+ setQueryNameInputValue('');
126
+ };
127
+ const changeQueryNameInputValue: React.ChangeEventHandler<
128
+ HTMLInputElement
129
+ > = (event) => setQueryNameInputValue(event.target.value);
130
+
131
+ // search text
132
+ const debouncedLoadQueries = useMemo(
133
+ () =>
134
+ debounce((input: string): void => {
135
+ flowResult(queryLoaderState.searchQueries(input)).catch(
136
+ applicationStore.alertUnhandledError,
137
+ );
138
+ }, 500),
139
+ [applicationStore.alertUnhandledError, queryLoaderState],
140
+ );
141
+ const onSearchTextChange: React.ChangeEventHandler<HTMLInputElement> = (
142
+ event,
143
+ ) => {
144
+ if (event.target.value !== queryLoaderState.searchText) {
145
+ queryLoaderState.setSearchText(event.target.value);
146
+ debouncedLoadQueries.cancel();
147
+ debouncedLoadQueries(event.target.value);
148
+ }
149
+ };
150
+ const clearQuerySearching = (): void => {
151
+ queryLoaderState.setSearchText('');
152
+ debouncedLoadQueries.cancel();
153
+ debouncedLoadQueries('');
154
+ };
155
+ const toggleShowCurrentUserQueriesOnly = (): void => {
156
+ queryLoaderState.setShowCurrentUserQueriesOnly(
157
+ !queryLoaderState.showCurrentUserQueriesOnly,
158
+ );
159
+ setIsMineOnly(!isMineOnly);
160
+ debouncedLoadQueries.cancel();
161
+ debouncedLoadQueries(queryLoaderState.searchText);
162
+ };
163
+ const toggleExtraFilters = (key: string): void => {
164
+ queryLoaderState.extraFilters.set(
165
+ key,
166
+ !queryLoaderState.extraFilters.get(key),
167
+ );
168
+ debouncedLoadQueries.cancel();
169
+ debouncedLoadQueries(queryLoaderState.searchText);
170
+ };
171
+
172
+ useEffect(() => {
173
+ flowResult(queryLoaderState.searchQueries('')).catch(
174
+ applicationStore.alertUnhandledError,
175
+ );
176
+ }, [applicationStore, queryLoaderState]);
177
+
178
+ useEffect(() => {
179
+ searchInputRef.current?.focus();
180
+ }, [queryLoaderState]);
181
+
182
+ // actions
183
+ const renameQuery =
184
+ (query: LightQuery): (() => void) =>
185
+ (): void => {
186
+ if (!queryLoaderState.isReadOnly) {
187
+ flowResult(
188
+ queryLoaderState.renameQuery(query.id, queryNameInputValue),
189
+ )
190
+ .catch(applicationStore.alertUnhandledError)
191
+ .finally(() => hideEditQueryNameInput());
192
+ }
193
+ };
194
+
195
+ const deleteQuery =
196
+ (query: LightQuery): (() => void) =>
197
+ (): void => {
198
+ if (!queryLoaderState.isReadOnly) {
199
+ flowResult(queryLoaderState.deleteQuery(query.id)).catch(
200
+ applicationStore.alertUnhandledError,
201
+ );
202
+ }
203
+ };
204
+
205
+ const showPreview = (queryId: string): void => {
206
+ flowResult(queryLoaderState.getPreviewQueryContent(queryId)).catch(
207
+ applicationStore.alertUnhandledError,
208
+ );
209
+ queryLoaderState.setShowPreviewViewer(true);
210
+ };
211
+
212
+ return (
213
+ <div className="query-loader">
214
+ <div className="query-loader__header">
215
+ <div className="query-loader__search">
216
+ <div className="query-loader__search__input__container">
217
+ <input
218
+ ref={searchInputRef}
219
+ className={clsx('query-loader__search__input input--dark', {
220
+ 'query-loader__search__input--searching':
221
+ queryLoaderState.searchText,
222
+ })}
223
+ onChange={onSearchTextChange}
224
+ value={queryLoaderState.searchText}
225
+ placeholder="Search for queries by name or ID"
226
+ />
227
+ {!queryLoaderState.searchText ? (
228
+ <div className="query-loader__search__input__search__icon">
229
+ <SearchIcon />
230
+ </div>
231
+ ) : (
232
+ <>
233
+ <button
234
+ className="query-loader__search__input__clear-btn"
235
+ tabIndex={-1}
236
+ onClick={clearQuerySearching}
237
+ title="Clear"
238
+ >
239
+ <TimesIcon />
240
+ </button>
241
+ </>
242
+ )}
243
+ </div>
244
+ </div>
245
+ <div className="query-loader__filter">
246
+ <div className="query-loader__filter__toggler">
247
+ <button
248
+ className={clsx('query-loader__filter__toggler__btn', {
249
+ 'query-loader__filter__toggler__btn--toggled': isMineOnly,
250
+ })}
251
+ onClick={toggleShowCurrentUserQueriesOnly}
252
+ tabIndex={-1}
253
+ >
254
+ Mine Only
255
+ </button>
256
+ {queryLoaderState.extraFilterOptions.length > 0 && (
257
+ <div className="query-loader__filter__extra__filters">
258
+ {Array.from(queryLoaderState.extraFilters.entries()).map(
259
+ ([key, value]) => (
260
+ <button
261
+ key={key}
262
+ className={clsx('query-loader__filter__toggler__btn', {
263
+ 'query-loader__filter__toggler__btn--toggled': value,
264
+ })}
265
+ onClick={(): void => toggleExtraFilters(key)}
266
+ tabIndex={-1}
267
+ >
268
+ {key}
269
+ </button>
270
+ ),
271
+ )}
272
+ </div>
273
+ )}
274
+ </div>
275
+ </div>
276
+ </div>
277
+ <div className="query-loader__content">
278
+ <PanelLoadingIndicator
279
+ isLoading={
280
+ queryLoaderState.searchQueriesState.isInProgress ||
281
+ queryLoaderState.renameQueryState.isInProgress ||
282
+ queryLoaderState.deleteQueryState.isInProgress ||
283
+ queryLoaderState.previewQueryState.isInProgress
284
+ }
285
+ />
286
+
287
+ <div className="query-loader__results">
288
+ {queryLoaderState.searchQueriesState.hasCompleted && (
289
+ <>
290
+ <div className="query-loader__results__summary">
291
+ {queryLoaderState.showingDefaultQueries ? (
292
+ queryLoaderState.generateDefaultQueriesSummaryText?.(
293
+ results,
294
+ ) ?? 'Refine your search to get better matches'
295
+ ) : results.length >= QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT ? (
296
+ <>
297
+ {`Found ${QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT}+ matches`}{' '}
298
+ <InfoCircleIcon
299
+ title="Some queries are not listed, refine your search to get better matches"
300
+ className="query-loader__results__summary__info"
301
+ />
302
+ </>
303
+ ) : (
304
+ `Found ${quantifyList(results, 'match', 'matches')}`
305
+ )}
306
+ </div>
307
+ {results
308
+ .slice(0, QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT)
309
+ .map((query, idx) => (
310
+ <div
311
+ className="query-loader__result"
312
+ title={`Click to ${loadActionLabel}...`}
313
+ key={query.id}
314
+ onClick={() => queryLoaderState.loadQuery(query)}
315
+ >
316
+ <div className="query-loader__result__content">
317
+ {showQueryNameEditInput === idx ? (
318
+ <div className="query-loader__result__title__editor">
319
+ <input
320
+ className="query-loader__result__title__editor__input input--dark"
321
+ spellCheck={false}
322
+ ref={queryRenameInputRef}
323
+ value={queryNameInputValue}
324
+ onChange={changeQueryNameInputValue}
325
+ onKeyDown={(event) => {
326
+ if (event.code === 'Enter') {
327
+ renameQuery(query)();
328
+ } else if (event.code === 'Escape') {
329
+ hideEditQueryNameInput();
330
+ }
331
+ }}
332
+ onBlur={() => hideEditQueryNameInput()}
333
+ />
334
+ </div>
335
+ ) : (
336
+ <div
337
+ className="query-loader__result__title"
338
+ title={query.name}
339
+ >
340
+ {query.name}
341
+ </div>
342
+ )}
343
+ <div className="query-loader__result__description">
344
+ <div className="query-loader__result__description__date__icon">
345
+ <LastModifiedIcon />
346
+ </div>
347
+ <div className="query-loader__result__description__date">
348
+ {query.lastUpdatedAt
349
+ ? formatDistanceToNow(
350
+ new Date(query.lastUpdatedAt),
351
+ {
352
+ includeSeconds: true,
353
+ addSuffix: true,
354
+ },
355
+ )
356
+ : '(unknown)'}
357
+ </div>
358
+ <div
359
+ className={clsx(
360
+ 'query-loader__result__description__author__icon',
361
+ {
362
+ 'query-loader__result__description__author__icon--owner':
363
+ query.isCurrentUserQuery,
364
+ },
365
+ )}
366
+ >
367
+ <UserIcon />
368
+ </div>
369
+ <div className="query-loader__result__description__author__name">
370
+ {query.isCurrentUserQuery ? (
371
+ <div
372
+ title={query.owner}
373
+ className="query-loader__result__description__owner"
374
+ >
375
+ Me
376
+ </div>
377
+ ) : (
378
+ query.owner
379
+ )}
380
+ </div>
381
+ </div>
382
+ </div>
383
+ <DropdownMenu
384
+ className="query-loader__result__actions-menu"
385
+ title="More Actions..."
386
+ content={
387
+ <MenuContent>
388
+ <MenuContentItem
389
+ onClick={(): void => showPreview(query.id)}
390
+ >
391
+ Show Query Preview
392
+ </MenuContentItem>
393
+ {!queryLoaderState.isReadOnly && (
394
+ <MenuContentItem
395
+ disabled={!query.isCurrentUserQuery}
396
+ onClick={deleteQuery(query)}
397
+ >
398
+ Delete
399
+ </MenuContentItem>
400
+ )}
401
+ {!queryLoaderState.isReadOnly && (
402
+ <MenuContentItem
403
+ disabled={!query.isCurrentUserQuery}
404
+ onClick={showEditQueryNameInput(
405
+ query.name,
406
+ idx,
407
+ )}
408
+ >
409
+ Rename
410
+ </MenuContentItem>
411
+ )}
412
+ </MenuContent>
413
+ }
414
+ menuProps={{
415
+ anchorOrigin: {
416
+ vertical: 'bottom',
417
+ horizontal: 'left',
418
+ },
419
+ transformOrigin: {
420
+ vertical: 'top',
421
+ horizontal: 'left',
422
+ },
423
+ elevation: 7,
424
+ }}
425
+ >
426
+ <MoreVerticalIcon />
427
+ </DropdownMenu>
428
+ <div className="query-loader__result__arrow">
429
+ <ThinChevronRightIcon />
430
+ </div>
431
+ </div>
432
+ ))}
433
+ </>
434
+ )}
435
+ {!queryLoaderState.searchQueriesState.hasCompleted && (
436
+ <BlankPanelContent>Loading queries...</BlankPanelContent>
437
+ )}
438
+ </div>
439
+ {queryLoaderState.showPreviewViewer &&
440
+ queryLoaderState.queryPreviewContent && (
441
+ <QueryPreviewViewer queryLoaderState={queryLoaderState} />
442
+ )}
443
+ </div>
444
+ </div>
445
+ );
446
+ },
447
+ );
448
+
449
+ export const QueryLoaderDialog = observer(
450
+ (props: {
451
+ queryLoaderState: QueryLoaderState;
452
+ title: string;
453
+ loadActionLabel?: string | undefined;
454
+ }) => {
455
+ const { queryLoaderState, title, loadActionLabel } = props;
456
+
457
+ const close = (): void => {
458
+ queryLoaderState.setQueryLoaderDialogOpen(false);
459
+ queryLoaderState.reset();
460
+ };
461
+
462
+ return (
463
+ <Dialog
464
+ open={queryLoaderState.isQueryLoaderDialogOpen}
465
+ onClose={close}
466
+ classes={{
467
+ root: 'query-loader__dialog',
468
+ container: 'query-loader__dialog__container',
469
+ }}
470
+ PaperProps={{
471
+ classes: { root: 'query-loader__dialog__body' },
472
+ }}
473
+ >
474
+ <Modal
475
+ darkMode={true}
476
+ className="modal query-loader__dialog__body__content"
477
+ >
478
+ <div className="modal query-loader__dialog__header">
479
+ <ModalTitle
480
+ className="query-loader__dialog__header__title"
481
+ title={title}
482
+ />
483
+ <button
484
+ className="query-loader__dialog__header__close-btn"
485
+ title="Close"
486
+ onClick={close}
487
+ >
488
+ <TimesIcon />
489
+ </button>
490
+ </div>
491
+ <div className="modal query-loader__dialog__content">
492
+ <QueryLoader
493
+ queryLoaderState={queryLoaderState}
494
+ loadActionLabel={loadActionLabel ?? title.toLowerCase()}
495
+ />
496
+ </div>
497
+ </Modal>
498
+ </Dialog>
499
+ );
500
+ },
501
+ );
@@ -15,6 +15,7 @@
15
15
  */
16
16
 
17
17
  import { TimesCircleIcon } from '@finos/legend-art';
18
+ import { quantifyList } from '@finos/legend-shared';
18
19
 
19
20
  export const QueryBuilderPanelIssueCountBadge: React.FC<{
20
21
  issues: string[] | undefined;
@@ -23,7 +24,7 @@ export const QueryBuilderPanelIssueCountBadge: React.FC<{
23
24
  if (!issues) {
24
25
  return null;
25
26
  }
26
- const labelText = `${issues.length} issue${issues.length > 1 ? 's' : ''}`;
27
+ const labelText = quantifyList(issues, 'issue');
27
28
  return (
28
29
  <div
29
30
  className="query-builder-panel-issue-count-badge"
package/src/index.ts CHANGED
@@ -70,3 +70,6 @@ export * from './stores/shared/ValueSpecificationEditorHelper.js';
70
70
 
71
71
  export * from './components/execution-plan/ExecutionPlanViewer.js';
72
72
  export * from './stores/execution-plan/ExecutionPlanState.js';
73
+ export * from './components/QueryLoader.js';
74
+ export * from './stores/QueryLoaderState.js';
75
+ export * from './stores/QueryBuilder_LegendApplicationPlugin_Extension.js';
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Copyright (c) 2020-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 type { LegendApplicationPlugin } from '@finos/legend-application';
18
+ import type { QueryBuilderState } from './QueryBuilderState.js';
19
+ import type { QuerySearchSpecification } from '@finos/legend-graph';
20
+
21
+ export type LoadQueryFilterOption = {
22
+ key: string;
23
+ label: (queryBuilderState: QueryBuilderState) => string | undefined;
24
+ filterFunction: (
25
+ searchSpecification: QuerySearchSpecification,
26
+ queryBuilderState: QueryBuilderState,
27
+ ) => QuerySearchSpecification;
28
+ };
29
+
30
+ export interface QueryBuilder_LegendApplicationPlugin_Extension
31
+ extends LegendApplicationPlugin {
32
+ /**
33
+ * Get the list of filter options for query loader.
34
+ */
35
+ getExtraLoadQueryFilterOptions?(): LoadQueryFilterOption[];
36
+ }