@finos/legend-query-builder 4.18.32 → 4.19.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 (74) hide show
  1. package/lib/__lib__/QueryBuilderSetting.d.ts +1 -2
  2. package/lib/__lib__/QueryBuilderSetting.d.ts.map +1 -1
  3. package/lib/__lib__/QueryBuilderSetting.js +0 -4
  4. package/lib/__lib__/QueryBuilderSetting.js.map +1 -1
  5. package/lib/components/QueryBuilder.d.ts.map +1 -1
  6. package/lib/components/QueryBuilder.js +3 -7
  7. package/lib/components/QueryBuilder.js.map +1 -1
  8. package/lib/components/QueryBuilderUnsupportedQueryEditor.d.ts.map +1 -1
  9. package/lib/components/QueryBuilderUnsupportedQueryEditor.js +1 -2
  10. package/lib/components/QueryBuilderUnsupportedQueryEditor.js.map +1 -1
  11. package/lib/components/QueryLoader.d.ts.map +1 -1
  12. package/lib/components/QueryLoader.js +215 -92
  13. package/lib/components/QueryLoader.js.map +1 -1
  14. package/lib/components/{QueryChat.d.ts → lakehouse/IngestionDefinitionArtifactViewer.d.ts} +7 -5
  15. package/lib/components/lakehouse/IngestionDefinitionArtifactViewer.d.ts.map +1 -0
  16. package/lib/components/lakehouse/IngestionDefinitionArtifactViewer.js +122 -0
  17. package/lib/components/lakehouse/IngestionDefinitionArtifactViewer.js.map +1 -0
  18. package/lib/components/result/tds/QueryBuilderTDSResultShared.d.ts +2 -1
  19. package/lib/components/result/tds/QueryBuilderTDSResultShared.d.ts.map +1 -1
  20. package/lib/components/result/tds/QueryBuilderTDSResultShared.js +2 -2
  21. package/lib/components/result/tds/QueryBuilderTDSResultShared.js.map +1 -1
  22. package/lib/components/workflows/DataProductQueryBuilder.d.ts.map +1 -1
  23. package/lib/components/workflows/DataProductQueryBuilder.js +7 -4
  24. package/lib/components/workflows/DataProductQueryBuilder.js.map +1 -1
  25. package/lib/data-access-overview.css +1 -1
  26. package/lib/graph-manager/QueryBuilderConfig.d.ts +0 -8
  27. package/lib/graph-manager/QueryBuilderConfig.d.ts.map +1 -1
  28. package/lib/graph-manager/QueryBuilderConfig.js +0 -10
  29. package/lib/graph-manager/QueryBuilderConfig.js.map +1 -1
  30. package/lib/index.css +1 -17
  31. package/lib/index.css.map +1 -1
  32. package/lib/index.d.ts +1 -1
  33. package/lib/index.d.ts.map +1 -1
  34. package/lib/index.js +1 -1
  35. package/lib/index.js.map +1 -1
  36. package/lib/package.json +1 -1
  37. package/lib/stores/QueryBuilderState.d.ts +0 -5
  38. package/lib/stores/QueryBuilderState.d.ts.map +1 -1
  39. package/lib/stores/QueryBuilderState.js +0 -19
  40. package/lib/stores/QueryBuilderState.js.map +1 -1
  41. package/lib/stores/QueryBuilder_LegendApplicationPlugin_Extension.d.ts +0 -5
  42. package/lib/stores/QueryBuilder_LegendApplicationPlugin_Extension.d.ts.map +1 -1
  43. package/lib/stores/QueryLoaderState.d.ts +39 -3
  44. package/lib/stores/QueryLoaderState.d.ts.map +1 -1
  45. package/lib/stores/QueryLoaderState.js +129 -2
  46. package/lib/stores/QueryLoaderState.js.map +1 -1
  47. package/lib/stores/workflows/dataProduct/DataProductQueryBuilderState.d.ts +6 -0
  48. package/lib/stores/workflows/dataProduct/DataProductQueryBuilderState.d.ts.map +1 -1
  49. package/lib/stores/workflows/dataProduct/DataProductQueryBuilderState.js +6 -3
  50. package/lib/stores/workflows/dataProduct/DataProductQueryBuilderState.js.map +1 -1
  51. package/package.json +7 -7
  52. package/src/__lib__/QueryBuilderSetting.ts +0 -4
  53. package/src/components/QueryBuilder.tsx +0 -31
  54. package/src/components/QueryBuilderUnsupportedQueryEditor.tsx +0 -7
  55. package/src/components/QueryLoader.tsx +708 -381
  56. package/src/components/lakehouse/IngestionDefinitionArtifactViewer.tsx +370 -0
  57. package/src/components/result/tds/QueryBuilderTDSResultShared.tsx +6 -3
  58. package/src/components/workflows/DataProductQueryBuilder.tsx +20 -5
  59. package/src/graph-manager/QueryBuilderConfig.ts +0 -11
  60. package/src/index.ts +1 -1
  61. package/src/stores/QueryBuilderState.ts +0 -26
  62. package/src/stores/QueryBuilder_LegendApplicationPlugin_Extension.ts +0 -9
  63. package/src/stores/QueryLoaderState.ts +173 -2
  64. package/src/stores/workflows/dataProduct/DataProductQueryBuilderState.ts +17 -8
  65. package/tsconfig.json +1 -2
  66. package/lib/components/QueryChat.d.ts.map +0 -1
  67. package/lib/components/QueryChat.js +0 -26
  68. package/lib/components/QueryChat.js.map +0 -1
  69. package/lib/stores/QueryChatState.d.ts +0 -19
  70. package/lib/stores/QueryChatState.d.ts.map +0 -1
  71. package/lib/stores/QueryChatState.js +0 -18
  72. package/lib/stores/QueryChatState.js.map +0 -1
  73. package/src/components/QueryChat.tsx +0 -38
  74. package/src/stores/QueryChatState.ts +0 -19
@@ -14,9 +14,13 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- import { useApplicationStore } from '@finos/legend-application';
17
+ import {
18
+ useApplicationStore,
19
+ ActionAlertType,
20
+ ActionAlertActionType,
21
+ } from '@finos/legend-application';
18
22
  import { CODE_EDITOR_LANGUAGE } from '@finos/legend-code-editor';
19
- import { CodeEditor } from '@finos/legend-lego/code-editor';
23
+ import { CodeEditor, CodeDiffView } from '@finos/legend-lego/code-editor';
20
24
  import {
21
25
  Dialog,
22
26
  Modal,
@@ -36,10 +40,12 @@ import {
36
40
  LastModifiedIcon,
37
41
  MoreVerticalIcon,
38
42
  ThinChevronRightIcon,
43
+ ArrowLeftIcon,
39
44
  InfoCircleIcon,
40
45
  CustomSelectorInput,
41
46
  CubesLoadingIndicator,
42
47
  CubesLoadingIndicatorIcon,
48
+ BlankPanelContent,
43
49
  } from '@finos/legend-art';
44
50
  import type { LightQuery, RawLambda } from '@finos/legend-graph';
45
51
  import {
@@ -51,25 +57,88 @@ import {
51
57
  } from '@finos/legend-shared';
52
58
  import { flowResult } from 'mobx';
53
59
  import { observer } from 'mobx-react-lite';
54
- import { useRef, useState, useMemo, useEffect } from 'react';
60
+ import { useRef, useState, useMemo, useEffect, type ReactNode } from 'react';
55
61
  import {
56
62
  QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT,
57
63
  SORT_BY_OPTIONS,
58
64
  type QueryLoaderState,
65
+ type QueryRevisionDiffInput,
59
66
  type SortByOption,
60
67
  } from '../stores/QueryLoaderState.js';
61
68
 
62
- const QueryPreviewViewer = observer(
63
- (props: { queryLoaderState: QueryLoaderState }) => {
64
- const { queryLoaderState } = props;
65
- const applicationStore = queryLoaderState.applicationStore;
66
- const close = (): void => {
67
- queryLoaderState.setShowPreviewViewer(false);
68
- };
69
+ const LATEST_REVISION_KEY = 'latest';
70
+
71
+ // Numeric ordering key for a revision `version`. The value is typed as a string
72
+ // but can arrive as a number at runtime, so coerce before parsing (e.g. 4 -> 4,
73
+ // "rev-2" -> 2); undefined/unparseable versions sort last.
74
+ const revisionSortKey = (version: string | undefined): number => {
75
+ const match = String(version ?? '').match(/\d+/);
76
+ return match ? Number(match[0]) : Number.NEGATIVE_INFINITY;
77
+ };
78
+
79
+ // Shared "last updated · owner" line rendered under a result title, used by both
80
+ // the query search results and the version-history rows. The owner is shown as a
81
+ // highlighted "Me" tag when it belongs to the current user.
82
+ const QueryResultDescription = observer(
83
+ (props: {
84
+ lastUpdatedAt: number | undefined;
85
+ owner: string | undefined;
86
+ isCurrentUser: boolean;
87
+ }) => {
88
+ const { lastUpdatedAt, owner, isCurrentUser } = props;
89
+ return (
90
+ <div className="query-loader__result__description">
91
+ <div className="query-loader__result__description__date__icon">
92
+ <LastModifiedIcon />
93
+ </div>
94
+ <div className="query-loader__result__description__date">
95
+ {lastUpdatedAt !== undefined
96
+ ? formatDistanceToNow(new Date(lastUpdatedAt), {
97
+ includeSeconds: true,
98
+ addSuffix: true,
99
+ })
100
+ : '(unknown)'}
101
+ </div>
102
+ <div
103
+ className={clsx('query-loader__result__description__author__icon', {
104
+ 'query-loader__result__description__author__icon--owner':
105
+ isCurrentUser,
106
+ })}
107
+ >
108
+ <UserIcon />
109
+ </div>
110
+ <div className="query-loader__result__description__author__name">
111
+ {isCurrentUser ? (
112
+ <div
113
+ title={owner}
114
+ className="query-loader__result__description__owner"
115
+ >
116
+ Me
117
+ </div>
118
+ ) : (
119
+ (owner ?? '(unknown)')
120
+ )}
121
+ </div>
122
+ </div>
123
+ );
124
+ },
125
+ );
126
+
127
+ // Shared read-only code modal (Pure grammar) used by the query preview and the
128
+ // version-history grammar diff — same shell, only the title and body differ.
129
+ const QueryLoaderCodeModal = observer(
130
+ (props: {
131
+ queryLoaderState: QueryLoaderState;
132
+ open: boolean;
133
+ title: string;
134
+ onClose: () => void;
135
+ children: ReactNode;
136
+ }) => {
137
+ const { queryLoaderState, open, title, onClose, children } = props;
69
138
  return (
70
139
  <Dialog
71
- open={queryLoaderState.showPreviewViewer}
72
- onClose={close}
140
+ open={open}
141
+ onClose={onClose}
73
142
  classes={{
74
143
  root: 'editor-modal__root-container',
75
144
  container: 'editor-modal__container',
@@ -79,26 +148,18 @@ const QueryPreviewViewer = observer(
79
148
  <Modal
80
149
  className="editor-modal"
81
150
  darkMode={
82
- !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled
151
+ !queryLoaderState.applicationStore.layoutService
152
+ .TEMPORARY__isLightColorThemeEnabled
83
153
  }
84
154
  >
85
- <ModalHeader
86
- title={
87
- guaranteeNonNullable(queryLoaderState.queryPreviewContent).name
88
- }
89
- />
90
- <ModalBody>
91
- <CodeEditor
92
- inputValue={
93
- guaranteeNonNullable(queryLoaderState.queryPreviewContent)
94
- .content
95
- }
96
- isReadOnly={true}
97
- language={CODE_EDITOR_LANGUAGE.PURE}
98
- />
99
- </ModalBody>
155
+ <ModalHeader title={title} />
156
+ <ModalBody>{children}</ModalBody>
100
157
  <ModalFooter>
101
- <ModalFooterButton onClick={close} text="Close" type="secondary" />
158
+ <ModalFooterButton
159
+ onClick={onClose}
160
+ text="Close"
161
+ type="secondary"
162
+ />
102
163
  </ModalFooter>
103
164
  </Modal>
104
165
  </Dialog>
@@ -106,6 +167,258 @@ const QueryPreviewViewer = observer(
106
167
  },
107
168
  );
108
169
 
170
+ const QueryPreviewViewer = observer(
171
+ (props: { queryLoaderState: QueryLoaderState }) => {
172
+ const { queryLoaderState } = props;
173
+ const content = guaranteeNonNullable(queryLoaderState.queryPreviewContent);
174
+ return (
175
+ <QueryLoaderCodeModal
176
+ queryLoaderState={queryLoaderState}
177
+ open={queryLoaderState.showPreviewViewer}
178
+ title={content.name}
179
+ onClose={(): void => queryLoaderState.setShowPreviewViewer(false)}
180
+ >
181
+ <CodeEditor
182
+ inputValue={content.content}
183
+ isReadOnly={true}
184
+ language={CODE_EDITOR_LANGUAGE.PURE}
185
+ />
186
+ </QueryLoaderCodeModal>
187
+ );
188
+ },
189
+ );
190
+
191
+ const QueryHistoryDiffViewer = observer(
192
+ (props: { queryLoaderState: QueryLoaderState }) => {
193
+ const { queryLoaderState } = props;
194
+ const labels = queryLoaderState.historyDiffLabels;
195
+ const grammars = queryLoaderState.historyDiffGrammars;
196
+ return (
197
+ <QueryLoaderCodeModal
198
+ queryLoaderState={queryLoaderState}
199
+ open={queryLoaderState.showHistoryDiff}
200
+ title={labels ? `${labels.from} → ${labels.to}` : 'Grammar Diff'}
201
+ onClose={(): void => queryLoaderState.setShowHistoryDiff(false)}
202
+ >
203
+ <PanelLoadingIndicator
204
+ isLoading={queryLoaderState.queryHistoryDiffState.isInProgress}
205
+ />
206
+ <div className="query-loader__history__diff">
207
+ <CodeDiffView
208
+ language={CODE_EDITOR_LANGUAGE.PURE}
209
+ from={grammars?.from ?? ''}
210
+ to={grammars?.to ?? ''}
211
+ />
212
+ </div>
213
+ </QueryLoaderCodeModal>
214
+ );
215
+ },
216
+ );
217
+
218
+ const QueryHistoryViewer = observer(
219
+ (props: { queryLoaderState: QueryLoaderState }) => {
220
+ const { queryLoaderState } = props;
221
+ const applicationStore = queryLoaderState.applicationStore;
222
+ const query = queryLoaderState.historyQuery;
223
+ const revisions = queryLoaderState.queryHistoryRevisions;
224
+ const selectedKeys = queryLoaderState.selectedRevisionKeysForDiff;
225
+
226
+ // Selectable snapshots for the grammar diff: the current (latest) version
227
+ // followed by each historical revision, newest first (highest `version`).
228
+ // The latest version's content is not part of the `/history` payload, so it
229
+ // is resolved lazily when comparing.
230
+ const orderedRevisions = [...revisions].sort(
231
+ (a, b) => revisionSortKey(b.version) - revisionSortKey(a.version),
232
+ );
233
+ const diffEntries = [
234
+ {
235
+ key: LATEST_REVISION_KEY,
236
+ label: 'Latest revision',
237
+ isLatest: true,
238
+ content: undefined as string | undefined,
239
+ revisionId: undefined as string | undefined,
240
+ lastUpdatedAt: query?.lastUpdatedAt,
241
+ owner: query?.owner,
242
+ },
243
+ ...orderedRevisions.map((revision, idx) => ({
244
+ key: revision.version ?? `revision-${idx}`,
245
+ label: `Revision ${revision.version}`,
246
+ isLatest: false,
247
+ content: revision.content as string | undefined,
248
+ revisionId: revision.version,
249
+ lastUpdatedAt: revision.lastUpdatedAt,
250
+ owner: revision.owner,
251
+ })),
252
+ ];
253
+
254
+ const back = (): void => {
255
+ queryLoaderState.setShowHistoryViewer(false);
256
+ queryLoaderState.clearHistoryDiffSelection();
257
+ };
258
+ const openRevision = (revisionId: string | undefined): void => {
259
+ queryLoaderState.setShowHistoryViewer(false);
260
+ queryLoaderState.setQueryLoaderDialogOpen(false);
261
+ queryLoaderState.loadQuery(guaranteeNonNullable(query), revisionId);
262
+ };
263
+ const compareSelected = (): void => {
264
+ // `diffEntries` runs newest → oldest, so ordering the two picks by their
265
+ // position here always diffs the older revision (from) into the newer one
266
+ // (to). This keeps the diff reading forward in time — additions show as
267
+ // additions — regardless of the order the checkboxes were ticked.
268
+ const [newer, older] = diffEntries.filter((entry) =>
269
+ selectedKeys.includes(entry.key),
270
+ );
271
+ if (newer && older) {
272
+ flowResult(
273
+ queryLoaderState.computeHistoryDiff(
274
+ older as QueryRevisionDiffInput,
275
+ newer as QueryRevisionDiffInput,
276
+ ),
277
+ ).catch(applicationStore.alertUnhandledError);
278
+ }
279
+ };
280
+ const confirmRevert = (label: string, revisionId: string): void => {
281
+ applicationStore.alertService.setActionAlertInfo({
282
+ message: `Revert this query to "${label}"? This saves the selected revision as the current version. Your version history is preserved, so you can revert back later.`,
283
+ type: ActionAlertType.CAUTION,
284
+ actions: [
285
+ {
286
+ label: 'Revert',
287
+ type: ActionAlertActionType.PROCEED_WITH_CAUTION,
288
+ handler: applicationStore.guardUnhandledError(() =>
289
+ flowResult(queryLoaderState.revertToRevision(revisionId)),
290
+ ),
291
+ },
292
+ {
293
+ label: 'Cancel',
294
+ type: ActionAlertActionType.PROCEED,
295
+ default: true,
296
+ },
297
+ ],
298
+ });
299
+ };
300
+
301
+ return (
302
+ <>
303
+ <div className="query-loader__header query-loader__header--history">
304
+ {!queryLoaderState.isHistoryViewerStandalone && (
305
+ <button
306
+ className="query-loader__history__back-btn"
307
+ title="Back to results"
308
+ onClick={back}
309
+ >
310
+ <ArrowLeftIcon />
311
+ </button>
312
+ )}
313
+ <div className="query-loader__history__title">
314
+ {`Query History${query ? ` - ${query.name}` : ''}`}
315
+ </div>
316
+ <button
317
+ className="query-loader__history__compare-btn"
318
+ title={
319
+ selectedKeys.length === 2
320
+ ? 'Compare the grammar of the two selected revisions'
321
+ : 'Select two revisions to compare their grammar'
322
+ }
323
+ disabled={selectedKeys.length !== 2}
324
+ onClick={compareSelected}
325
+ >
326
+ Compare
327
+ </button>
328
+ </div>
329
+ <div className="query-loader__content">
330
+ <PanelLoadingIndicator
331
+ isLoading={
332
+ queryLoaderState.queryHistoryState.isInProgress ||
333
+ queryLoaderState.queryHistoryDiffState.isInProgress
334
+ }
335
+ />
336
+ <div className="query-loader__results">
337
+ {/* The `/history` endpoint only returns non-current revisions, so
338
+ the latest (current) version is surfaced explicitly. */}
339
+ {diffEntries.map((entry) => (
340
+ <div
341
+ key={entry.key}
342
+ className="query-loader__result query-loader__result--history"
343
+ >
344
+ <label
345
+ className="query-loader__result__diff-select"
346
+ title="Select this revision for a grammar diff"
347
+ >
348
+ <input
349
+ type="checkbox"
350
+ checked={selectedKeys.includes(entry.key)}
351
+ onChange={(): void =>
352
+ queryLoaderState.toggleRevisionForDiff(entry.key)
353
+ }
354
+ />
355
+ </label>
356
+ <div
357
+ className="query-loader__result__content"
358
+ title={
359
+ entry.isLatest
360
+ ? 'Load the latest revision'
361
+ : 'Load this revision'
362
+ }
363
+ onClick={(): void => openRevision(entry.revisionId)}
364
+ >
365
+ <div className="query-loader__result__title">
366
+ {entry.label}
367
+ </div>
368
+ <QueryResultDescription
369
+ lastUpdatedAt={entry.lastUpdatedAt}
370
+ owner={entry.owner}
371
+ isCurrentUser={
372
+ entry.owner ===
373
+ applicationStore.identityService.currentUser
374
+ }
375
+ />
376
+ </div>
377
+ {!queryLoaderState.isReadOnly &&
378
+ !entry.isLatest &&
379
+ entry.revisionId !== undefined && (
380
+ <button
381
+ className="query-loader__result__revert-btn"
382
+ title="Revert the query to this revision (saves it as the current version)"
383
+ onClick={(): void =>
384
+ confirmRevert(
385
+ entry.label,
386
+ guaranteeNonNullable(entry.revisionId),
387
+ )
388
+ }
389
+ >
390
+ Revert
391
+ </button>
392
+ )}
393
+ <div
394
+ className="query-loader__result__arrow"
395
+ title={
396
+ entry.isLatest
397
+ ? 'Load the latest revision'
398
+ : 'Load this revision'
399
+ }
400
+ onClick={(): void => openRevision(entry.revisionId)}
401
+ >
402
+ <ThinChevronRightIcon />
403
+ </div>
404
+ </div>
405
+ ))}
406
+ </div>
407
+ {queryLoaderState.queryHistoryState.hasCompleted &&
408
+ revisions.length === 0 && (
409
+ <BlankPanelContent>
410
+ No earlier revisions available
411
+ </BlankPanelContent>
412
+ )}
413
+ </div>
414
+ {queryLoaderState.showHistoryDiff && (
415
+ <QueryHistoryDiffViewer queryLoaderState={queryLoaderState} />
416
+ )}
417
+ </>
418
+ );
419
+ },
420
+ );
421
+
109
422
  export const QueryLoader = observer(
110
423
  (props: { queryLoaderState: QueryLoaderState; loadActionLabel: string }) => {
111
424
  const { queryLoaderState, loadActionLabel } = props;
@@ -268,386 +581,387 @@ export const QueryLoader = observer(
268
581
  ).catch(applicationStore.alertUnhandledError);
269
582
  queryLoaderState.setShowPreviewViewer(true);
270
583
  };
584
+ const showHistory = (query: LightQuery): void => {
585
+ flowResult(queryLoaderState.getQueryHistory(query)).catch(
586
+ applicationStore.alertUnhandledError,
587
+ );
588
+ };
271
589
 
272
590
  return (
273
591
  <div className="query-loader">
274
- <div className="query-loader__header">
275
- <div className="query-loader__search">
276
- <div className="query-loader__search__input__container">
277
- <input
278
- ref={searchInputRef}
279
- className={clsx('query-loader__search__input input--dark', {
280
- 'query-loader__search__input--searching':
281
- queryLoaderState.searchText,
282
- })}
283
- onChange={onSearchTextChange}
284
- value={queryLoaderState.searchText}
285
- placeholder="Search for queries by name or ID"
286
- />
287
- {!queryLoaderState.searchText ? (
288
- <div className="query-loader__search__input__search__icon">
289
- <SearchIcon />
290
- </div>
291
- ) : (
292
- <>
293
- <button
294
- className="query-loader__search__input__clear-btn"
295
- tabIndex={-1}
296
- onClick={clearSearchResults}
297
- title="Clear"
298
- >
299
- <TimesIcon />
300
- </button>
301
- </>
302
- )}
303
- </div>
304
- </div>
305
- <div className="query-loader__filter">
306
- <div className="query-loader__filter__toggler">
307
- <button
308
- className={clsx('query-loader__filter__toggler__btn', {
309
- 'query-loader__filter__toggler__btn--toggled': isMineOnly,
310
- })}
311
- onClick={toggleShowCurrentUserQueriesOnly}
312
- disabled={queryLoaderState.isCuratedTemplateToggled}
313
- title={
314
- queryLoaderState.isCuratedTemplateToggled
315
- ? 'current fitler is disabled when `Curated Template Query` is on'
316
- : 'click to add filter'
317
- }
318
- tabIndex={-1}
319
- >
320
- Mine Only
321
- </button>
322
- {queryLoaderState.extraFilterOptions.length > 0 && (
323
- <div className="query-loader__filter__extra__filters">
324
- {Array.from(queryLoaderState.extraFilters.entries()).map(
325
- ([key, value]) => (
592
+ {queryLoaderState.showHistoryViewer ? (
593
+ <QueryHistoryViewer queryLoaderState={queryLoaderState} />
594
+ ) : (
595
+ <>
596
+ <div className="query-loader__header">
597
+ <div className="query-loader__search">
598
+ <div className="query-loader__search__input__container">
599
+ <input
600
+ ref={searchInputRef}
601
+ className={clsx('query-loader__search__input input--dark', {
602
+ 'query-loader__search__input--searching':
603
+ queryLoaderState.searchText,
604
+ })}
605
+ onChange={onSearchTextChange}
606
+ value={queryLoaderState.searchText}
607
+ placeholder="Search for queries by name or ID"
608
+ />
609
+ {!queryLoaderState.searchText ? (
610
+ <div className="query-loader__search__input__search__icon">
611
+ <SearchIcon />
612
+ </div>
613
+ ) : (
614
+ <>
326
615
  <button
327
- key={key}
328
- className={clsx('query-loader__filter__toggler__btn', {
329
- 'query-loader__filter__toggler__btn--toggled': value,
330
- })}
331
- disabled={queryLoaderState.isCuratedTemplateToggled}
332
- title={
333
- queryLoaderState.isCuratedTemplateToggled
334
- ? 'current fitler is disabled when `Curated Template Query` is on'
335
- : 'click to add filter'
336
- }
337
- onClick={(): void => toggleExtraFilters(key)}
616
+ className="query-loader__search__input__clear-btn"
338
617
  tabIndex={-1}
618
+ onClick={clearSearchResults}
619
+ title="Clear"
339
620
  >
340
- {key}
621
+ <TimesIcon />
341
622
  </button>
342
- ),
623
+ </>
343
624
  )}
344
625
  </div>
345
- )}
346
- {queryLoaderState.extraQueryFilterOptionsRelatedToTemplateQuery
347
- .length > 0 && (
348
- <div className="query-loader__filter__extra__filters">
626
+ </div>
627
+ <div className="query-loader__filter">
628
+ <div className="query-loader__filter__toggler">
349
629
  <button
350
630
  className={clsx('query-loader__filter__toggler__btn', {
351
- 'query-loader__filter__toggler__btn--toggled':
352
- queryLoaderState.isCuratedTemplateToggled,
631
+ 'query-loader__filter__toggler__btn--toggled': isMineOnly,
353
632
  })}
354
- onClick={toggleCuratedTemplate}
633
+ onClick={toggleShowCurrentUserQueriesOnly}
634
+ disabled={queryLoaderState.isCuratedTemplateToggled}
635
+ title={
636
+ queryLoaderState.isCuratedTemplateToggled
637
+ ? 'current fitler is disabled when `Curated Template Query` is on'
638
+ : 'click to add filter'
639
+ }
355
640
  tabIndex={-1}
356
641
  >
357
- Curated Template Query
642
+ Mine Only
358
643
  </button>
359
- </div>
360
- )}
361
- </div>
362
- </div>
363
- </div>
364
- <div className="query-loader__content">
365
- <PanelLoadingIndicator
366
- isLoading={
367
- queryLoaderState.searchQueriesState.isInProgress ||
368
- queryLoaderState.renameQueryState.isInProgress ||
369
- queryLoaderState.deleteQueryState.isInProgress ||
370
- queryLoaderState.previewQueryState.isInProgress
371
- }
372
- />
373
- <div className="query-loader__results">
374
- {queryLoaderState.searchQueriesState.hasCompleted && (
375
- <>
376
- <div className="query-loader__results__summary">
377
- {queryLoaderState.showingDefaultQueries ? (
378
- (queryLoaderState.generateDefaultQueriesSummaryText?.(
379
- searchResults,
380
- ) ?? 'Refine your search to get better matches')
381
- ) : !queryLoaderState.isCuratedTemplateToggled ? (
382
- searchResults.length >=
383
- QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT ? (
384
- <>
385
- {`Found ${QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT}+ matches`}{' '}
386
- <InfoCircleIcon
387
- title="Some queries are not listed, refine your search to get better matches"
388
- className="query-loader__results__summary__info"
389
- />
390
- </>
391
- ) : (
392
- `Found ${quantifyList(searchResults, 'match', 'matches')}`
393
- )
394
- ) : curatedTemplateQueries.length >=
395
- QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT ? (
396
- <>
397
- {`Found ${QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT}+ matches`}{' '}
398
- <InfoCircleIcon
399
- title="Some queries are not listed, refine your search to get better matches"
400
- className="query-loader__results__summary__info"
401
- />
402
- </>
403
- ) : (
404
- `Found ${quantifyList(
405
- curatedTemplateQueries,
406
- 'match',
407
- 'matches',
408
- )}`
644
+ {queryLoaderState.extraFilterOptions.length > 0 && (
645
+ <div className="query-loader__filter__extra__filters">
646
+ {Array.from(queryLoaderState.extraFilters.entries()).map(
647
+ ([key, value]) => (
648
+ <button
649
+ key={key}
650
+ className={clsx(
651
+ 'query-loader__filter__toggler__btn',
652
+ {
653
+ 'query-loader__filter__toggler__btn--toggled':
654
+ value,
655
+ },
656
+ )}
657
+ disabled={queryLoaderState.isCuratedTemplateToggled}
658
+ title={
659
+ queryLoaderState.isCuratedTemplateToggled
660
+ ? 'current fitler is disabled when `Curated Template Query` is on'
661
+ : 'click to add filter'
662
+ }
663
+ onClick={(): void => toggleExtraFilters(key)}
664
+ tabIndex={-1}
665
+ >
666
+ {key}
667
+ </button>
668
+ ),
669
+ )}
670
+ </div>
409
671
  )}
410
- {queryLoaderState.canPerformAdvancedSearch(
411
- queryLoaderState.searchText,
412
- ) &&
413
- !queryLoaderState.isCuratedTemplateToggled && (
414
- <div className="query-loader__results__sort-by">
415
- <div className="query-loader__results__sort-by__name">
416
- Sort By
417
- </div>
418
- <CustomSelectorInput
419
- className="query-loader__results__sort-by__selector"
420
- options={sortOptions}
421
- onChange={(option: SortByOption) => {
422
- applySort(option);
423
- }}
424
- value={{
425
- label: queryLoaderState.sortBy,
426
- value: queryLoaderState.sortBy,
427
- }}
428
- darkMode={
429
- !applicationStore.layoutService
430
- .TEMPORARY__isLightColorThemeEnabled
431
- }
432
- />
433
- </div>
434
- )}
435
- </div>
436
- {!queryLoaderState.isCuratedTemplateToggled &&
437
- searchResults
438
- .slice(0, QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT)
439
- .map((query, idx) => (
440
- <div
441
- className="query-loader__result"
442
- title={`Click to ${loadActionLabel}...`}
443
- key={query.id}
444
- onClick={() => queryLoaderState.loadQuery(query)}
672
+ {queryLoaderState
673
+ .extraQueryFilterOptionsRelatedToTemplateQuery.length >
674
+ 0 && (
675
+ <div className="query-loader__filter__extra__filters">
676
+ <button
677
+ className={clsx('query-loader__filter__toggler__btn', {
678
+ 'query-loader__filter__toggler__btn--toggled':
679
+ queryLoaderState.isCuratedTemplateToggled,
680
+ })}
681
+ onClick={toggleCuratedTemplate}
682
+ tabIndex={-1}
445
683
  >
446
- <div className="query-loader__result__content">
447
- {showQueryNameEditInput === idx ? (
448
- <div className="query-loader__result__title__editor">
449
- <input
450
- className="query-loader__result__title__editor__input input--dark"
451
- spellCheck={false}
452
- ref={queryRenameInputRef}
453
- value={queryNameInputValue}
454
- onChange={changeQueryNameInputValue}
455
- onKeyDown={(event) => {
456
- if (event.code === 'Enter') {
457
- event.stopPropagation();
458
- renameQuery(query)();
459
- } else if (event.code === 'Escape') {
460
- event.stopPropagation();
461
- hideEditQueryNameInput();
462
- }
463
- }}
464
- onBlur={() => hideEditQueryNameInput()}
465
- // avoid clicking on the input causing the call to load query
466
- onClick={(event) => event.stopPropagation()}
467
- />
468
- </div>
469
- ) : (
470
- <div
471
- className="query-loader__result__title"
472
- title={query.name}
473
- >
474
- {query.name}
475
- </div>
476
- )}
477
- <div className="query-loader__result__description">
478
- <div className="query-loader__result__description__date__icon">
479
- <LastModifiedIcon />
480
- </div>
481
- <div className="query-loader__result__description__date">
482
- {query.lastUpdatedAt
483
- ? formatDistanceToNow(
484
- new Date(query.lastUpdatedAt),
485
- {
486
- includeSeconds: true,
487
- addSuffix: true,
488
- },
489
- )
490
- : '(unknown)'}
684
+ Curated Template Query
685
+ </button>
686
+ </div>
687
+ )}
688
+ </div>
689
+ </div>
690
+ </div>
691
+ <div className="query-loader__content">
692
+ <PanelLoadingIndicator
693
+ isLoading={
694
+ queryLoaderState.searchQueriesState.isInProgress ||
695
+ queryLoaderState.renameQueryState.isInProgress ||
696
+ queryLoaderState.deleteQueryState.isInProgress ||
697
+ queryLoaderState.previewQueryState.isInProgress
698
+ }
699
+ />
700
+ <div className="query-loader__results">
701
+ {queryLoaderState.searchQueriesState.hasCompleted && (
702
+ <>
703
+ <div className="query-loader__results__summary">
704
+ {queryLoaderState.showingDefaultQueries ? (
705
+ (queryLoaderState.generateDefaultQueriesSummaryText?.(
706
+ searchResults,
707
+ ) ?? 'Refine your search to get better matches')
708
+ ) : !queryLoaderState.isCuratedTemplateToggled ? (
709
+ searchResults.length >=
710
+ QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT ? (
711
+ <>
712
+ {`Found ${QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT}+ matches`}{' '}
713
+ <InfoCircleIcon
714
+ title="Some queries are not listed, refine your search to get better matches"
715
+ className="query-loader__results__summary__info"
716
+ />
717
+ </>
718
+ ) : (
719
+ `Found ${quantifyList(searchResults, 'match', 'matches')}`
720
+ )
721
+ ) : curatedTemplateQueries.length >=
722
+ QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT ? (
723
+ <>
724
+ {`Found ${QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT}+ matches`}{' '}
725
+ <InfoCircleIcon
726
+ title="Some queries are not listed, refine your search to get better matches"
727
+ className="query-loader__results__summary__info"
728
+ />
729
+ </>
730
+ ) : (
731
+ `Found ${quantifyList(
732
+ curatedTemplateQueries,
733
+ 'match',
734
+ 'matches',
735
+ )}`
736
+ )}
737
+ {queryLoaderState.canPerformAdvancedSearch(
738
+ queryLoaderState.searchText,
739
+ ) &&
740
+ !queryLoaderState.isCuratedTemplateToggled && (
741
+ <div className="query-loader__results__sort-by">
742
+ <div className="query-loader__results__sort-by__name">
743
+ Sort By
491
744
  </div>
745
+ <CustomSelectorInput
746
+ className="query-loader__results__sort-by__selector"
747
+ options={sortOptions}
748
+ onChange={(option: SortByOption) => {
749
+ applySort(option);
750
+ }}
751
+ value={{
752
+ label: queryLoaderState.sortBy,
753
+ value: queryLoaderState.sortBy,
754
+ }}
755
+ darkMode={
756
+ !applicationStore.layoutService
757
+ .TEMPORARY__isLightColorThemeEnabled
758
+ }
759
+ />
760
+ </div>
761
+ )}
762
+ </div>
763
+ {!queryLoaderState.isCuratedTemplateToggled &&
764
+ searchResults
765
+ .slice(0, QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT)
766
+ .map((query, idx) => (
767
+ <div className="query-loader__result" key={query.id}>
492
768
  <div
493
- className={clsx(
494
- 'query-loader__result__description__author__icon',
495
- {
496
- 'query-loader__result__description__author__icon--owner':
497
- query.isCurrentUserQuery,
498
- },
499
- )}
769
+ className="query-loader__result__content"
770
+ title={`Click to ${loadActionLabel}...`}
771
+ onClick={() => queryLoaderState.loadQuery(query)}
500
772
  >
501
- <UserIcon />
502
- </div>
503
- <div className="query-loader__result__description__author__name">
504
- {query.isCurrentUserQuery ? (
773
+ {showQueryNameEditInput === idx ? (
774
+ <div className="query-loader__result__title__editor">
775
+ <input
776
+ className="query-loader__result__title__editor__input input--dark"
777
+ spellCheck={false}
778
+ ref={queryRenameInputRef}
779
+ value={queryNameInputValue}
780
+ onChange={changeQueryNameInputValue}
781
+ onKeyDown={(event) => {
782
+ if (event.code === 'Enter') {
783
+ event.stopPropagation();
784
+ renameQuery(query)();
785
+ } else if (event.code === 'Escape') {
786
+ event.stopPropagation();
787
+ hideEditQueryNameInput();
788
+ }
789
+ }}
790
+ onBlur={() => hideEditQueryNameInput()}
791
+ // avoid clicking on the input causing the call to load query
792
+ onClick={(event) => event.stopPropagation()}
793
+ />
794
+ </div>
795
+ ) : (
505
796
  <div
506
- title={query.owner}
507
- className="query-loader__result__description__owner"
797
+ className="query-loader__result__title"
798
+ title={query.name}
508
799
  >
509
- Me
800
+ {query.name}
510
801
  </div>
511
- ) : (
512
- query.owner
513
802
  )}
803
+ <QueryResultDescription
804
+ lastUpdatedAt={query.lastUpdatedAt}
805
+ owner={query.owner}
806
+ isCurrentUser={query.isCurrentUserQuery}
807
+ />
808
+ </div>
809
+ <ControlledDropdownMenu
810
+ className="query-loader__result__actions-menu"
811
+ title="More Actions..."
812
+ content={
813
+ <MenuContent>
814
+ <MenuContentItem
815
+ onClick={(): void => showPreview(query.id)}
816
+ >
817
+ Show Query Preview
818
+ </MenuContentItem>
819
+ <MenuContentItem
820
+ onClick={(): void => showHistory(query)}
821
+ >
822
+ Show Query History
823
+ </MenuContentItem>
824
+ {!queryLoaderState.isReadOnly && (
825
+ <MenuContentItem
826
+ disabled={!query.isCurrentUserQuery}
827
+ onClick={deleteQuery(query)}
828
+ >
829
+ Delete
830
+ </MenuContentItem>
831
+ )}
832
+ {!queryLoaderState.isReadOnly && (
833
+ <MenuContentItem
834
+ disabled={!query.isCurrentUserQuery}
835
+ onClick={showEditQueryNameInput(
836
+ query.name,
837
+ idx,
838
+ )}
839
+ >
840
+ Rename
841
+ </MenuContentItem>
842
+ )}
843
+ </MenuContent>
844
+ }
845
+ menuProps={{
846
+ anchorOrigin: {
847
+ vertical: 'bottom',
848
+ horizontal: 'left',
849
+ },
850
+ transformOrigin: {
851
+ vertical: 'top',
852
+ horizontal: 'left',
853
+ },
854
+ elevation: 7,
855
+ }}
856
+ >
857
+ <MoreVerticalIcon />
858
+ </ControlledDropdownMenu>
859
+ <div
860
+ className="query-loader__result__arrow"
861
+ title={`Click to ${loadActionLabel}...`}
862
+ onClick={() => queryLoaderState.loadQuery(query)}
863
+ >
864
+ <ThinChevronRightIcon />
514
865
  </div>
515
866
  </div>
516
- </div>
517
- <ControlledDropdownMenu
518
- className="query-loader__result__actions-menu"
519
- title="More Actions..."
520
- content={
521
- <MenuContent>
522
- <MenuContentItem
523
- onClick={(): void => showPreview(query.id)}
867
+ ))}
868
+ {queryLoaderState.queryBuilderState &&
869
+ queryLoaderState.isCuratedTemplateToggled &&
870
+ loadCuratedTemplateQuery &&
871
+ curatedTemplateQueries
872
+ .slice(0, QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT)
873
+ .map((templateQuery) => {
874
+ const loadTemplateQuery = (): void => {
875
+ flowResult(
876
+ loadCuratedTemplateQuery()(
877
+ templateQuery,
878
+ guaranteeNonNullable(
879
+ queryLoaderState.queryBuilderState,
880
+ ),
881
+ ),
882
+ );
883
+ queryLoaderState.setQueryLoaderDialogOpen(false);
884
+ };
885
+ return (
886
+ <div
887
+ className="query-loader__result"
888
+ key={templateQuery.title}
889
+ >
890
+ <div
891
+ className="query-loader__result__content"
892
+ title={`Click to ${loadActionLabel}...`}
893
+ onClick={loadTemplateQuery}
524
894
  >
525
- Show Query Preview
526
- </MenuContentItem>
527
- {!queryLoaderState.isReadOnly && (
528
- <MenuContentItem
529
- disabled={!query.isCurrentUserQuery}
530
- onClick={deleteQuery(query)}
531
- >
532
- Delete
533
- </MenuContentItem>
534
- )}
535
- {!queryLoaderState.isReadOnly && (
536
- <MenuContentItem
537
- disabled={!query.isCurrentUserQuery}
538
- onClick={showEditQueryNameInput(
539
- query.name,
540
- idx,
541
- )}
895
+ <div
896
+ className="query-loader__result__title"
897
+ title={templateQuery.title}
542
898
  >
543
- Rename
544
- </MenuContentItem>
545
- )}
546
- </MenuContent>
547
- }
548
- menuProps={{
549
- anchorOrigin: {
550
- vertical: 'bottom',
551
- horizontal: 'left',
552
- },
553
- transformOrigin: {
554
- vertical: 'top',
555
- horizontal: 'left',
556
- },
557
- elevation: 7,
558
- }}
559
- >
560
- <MoreVerticalIcon />
561
- </ControlledDropdownMenu>
562
- <div className="query-loader__result__arrow">
563
- <ThinChevronRightIcon />
564
- </div>
565
- </div>
566
- ))}
567
- {queryLoaderState.queryBuilderState &&
568
- queryLoaderState.isCuratedTemplateToggled &&
569
- loadCuratedTemplateQuery &&
570
- curatedTemplateQueries
571
- .slice(0, QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT)
572
- .map((templateQuery, idx) => (
573
- <div
574
- className="query-loader__result"
575
- title={`Click to ${loadActionLabel}...`}
576
- key={templateQuery.title}
577
- onClick={() => {
578
- flowResult(
579
- loadCuratedTemplateQuery()(
580
- templateQuery,
581
- guaranteeNonNullable(
582
- queryLoaderState.queryBuilderState,
583
- ),
584
- ),
585
- );
586
- queryLoaderState.setQueryLoaderDialogOpen(false);
587
- }}
588
- >
589
- <div className="query-loader__result__content">
590
- <div
591
- className="query-loader__result__title"
592
- title={templateQuery.title}
593
- >
594
- {templateQuery.title}
595
- </div>
596
- <div className="query-loader__result__description">
597
- {templateQuery.description}
598
- </div>
599
- </div>
600
- <ControlledDropdownMenu
601
- className="query-loader__result__actions-menu"
602
- title="More Actions..."
603
- content={
604
- <MenuContent>
605
- <MenuContentItem
606
- onClick={(): void =>
607
- showPreview(undefined, {
608
- queryContent: templateQuery.query,
609
- queryName: templateQuery.title,
610
- })
899
+ {templateQuery.title}
900
+ </div>
901
+ <div className="query-loader__result__description">
902
+ {templateQuery.description}
903
+ </div>
904
+ </div>
905
+ <ControlledDropdownMenu
906
+ className="query-loader__result__actions-menu"
907
+ title="More Actions..."
908
+ content={
909
+ <MenuContent>
910
+ <MenuContentItem
911
+ onClick={(): void =>
912
+ showPreview(undefined, {
913
+ queryContent: templateQuery.query,
914
+ queryName: templateQuery.title,
915
+ })
916
+ }
917
+ >
918
+ Show Query Preview
919
+ </MenuContentItem>
920
+ </MenuContent>
611
921
  }
922
+ menuProps={{
923
+ anchorOrigin: {
924
+ vertical: 'bottom',
925
+ horizontal: 'left',
926
+ },
927
+ transformOrigin: {
928
+ vertical: 'top',
929
+ horizontal: 'left',
930
+ },
931
+ elevation: 7,
932
+ }}
612
933
  >
613
- Show Query Preview
614
- </MenuContentItem>
615
- </MenuContent>
616
- }
617
- menuProps={{
618
- anchorOrigin: {
619
- vertical: 'bottom',
620
- horizontal: 'left',
621
- },
622
- transformOrigin: {
623
- vertical: 'top',
624
- horizontal: 'left',
625
- },
626
- elevation: 7,
627
- }}
628
- >
629
- <MoreVerticalIcon />
630
- </ControlledDropdownMenu>
631
- <div className="query-loader__result__arrow">
632
- <ThinChevronRightIcon />
633
- </div>
634
- </div>
635
- ))}
636
- </>
637
- )}
638
- {!queryLoaderState.searchQueriesState.hasCompleted && (
639
- <CubesLoadingIndicator
640
- isLoading={!queryLoaderState.searchQueriesState.hasCompleted}
641
- >
642
- <CubesLoadingIndicatorIcon />
643
- </CubesLoadingIndicator>
644
- )}
645
- </div>
646
- {queryLoaderState.showPreviewViewer &&
647
- queryLoaderState.queryPreviewContent && (
648
- <QueryPreviewViewer queryLoaderState={queryLoaderState} />
649
- )}
650
- </div>
934
+ <MoreVerticalIcon />
935
+ </ControlledDropdownMenu>
936
+ <div
937
+ className="query-loader__result__arrow"
938
+ title={`Click to ${loadActionLabel}...`}
939
+ onClick={loadTemplateQuery}
940
+ >
941
+ <ThinChevronRightIcon />
942
+ </div>
943
+ </div>
944
+ );
945
+ })}
946
+ </>
947
+ )}
948
+ {!queryLoaderState.searchQueriesState.hasCompleted && (
949
+ <CubesLoadingIndicator
950
+ isLoading={
951
+ !queryLoaderState.searchQueriesState.hasCompleted
952
+ }
953
+ >
954
+ <CubesLoadingIndicatorIcon />
955
+ </CubesLoadingIndicator>
956
+ )}
957
+ </div>
958
+ </div>
959
+ </>
960
+ )}
961
+ {queryLoaderState.showPreviewViewer &&
962
+ queryLoaderState.queryPreviewContent && (
963
+ <QueryPreviewViewer queryLoaderState={queryLoaderState} />
964
+ )}
651
965
  </div>
652
966
  );
653
967
  },
@@ -663,6 +977,13 @@ export const QueryLoaderDialog = observer(
663
977
  const applicationStore = queryLoaderState.applicationStore;
664
978
 
665
979
  const close = (): void => {
980
+ // while the grammar-diff dialog is open, a dismiss (Close/Escape/backdrop)
981
+ // that reaches this parent should only close the diff and return to the
982
+ // history list — not tear down the whole loader
983
+ if (queryLoaderState.showHistoryDiff) {
984
+ queryLoaderState.setShowHistoryDiff(false);
985
+ return;
986
+ }
666
987
  queryLoaderState.setQueryLoaderDialogOpen(false);
667
988
  queryLoaderState.reset();
668
989
  };
@@ -677,7 +998,9 @@ export const QueryLoaderDialog = observer(
677
998
  }}
678
999
  slotProps={{
679
1000
  paper: {
680
- classes: { root: 'query-loader__dialog__body' },
1001
+ classes: {
1002
+ root: 'query-loader__dialog__body',
1003
+ },
681
1004
  },
682
1005
  }}
683
1006
  >
@@ -690,7 +1013,11 @@ export const QueryLoaderDialog = observer(
690
1013
  <div className="modal query-loader__dialog__header">
691
1014
  <ModalTitle
692
1015
  className="query-loader__dialog__header__title"
693
- title={title}
1016
+ title={
1017
+ queryLoaderState.isHistoryViewerStandalone
1018
+ ? 'Query History'
1019
+ : title
1020
+ }
694
1021
  />
695
1022
  <button
696
1023
  className="query-loader__dialog__header__close-btn"