@finos/legend-query-builder 3.1.0 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ }