@finos/legend-query-builder 4.18.1 → 4.18.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finos/legend-query-builder",
3
- "version": "4.18.1",
3
+ "version": "4.18.2",
4
4
  "description": "Legend query builder core",
5
5
  "keywords": [
6
6
  "legend",
@@ -43,15 +43,15 @@
43
43
  "test:watch": "jest --watch"
44
44
  },
45
45
  "dependencies": {
46
- "@finos/legend-application": "16.0.104",
47
- "@finos/legend-art": "7.1.146",
48
- "@finos/legend-code-editor": "2.0.165",
49
- "@finos/legend-data-cube": "0.3.82",
50
- "@finos/legend-graph": "32.5.1",
51
- "@finos/legend-lego": "2.0.184",
52
- "@finos/legend-server-depot": "6.1.10",
53
- "@finos/legend-shared": "11.0.23",
54
- "@finos/legend-storage": "3.0.144",
46
+ "@finos/legend-application": "16.0.105",
47
+ "@finos/legend-art": "7.1.147",
48
+ "@finos/legend-code-editor": "2.0.166",
49
+ "@finos/legend-data-cube": "0.3.83",
50
+ "@finos/legend-graph": "32.5.2",
51
+ "@finos/legend-lego": "2.0.185",
52
+ "@finos/legend-server-depot": "6.1.11",
53
+ "@finos/legend-shared": "11.0.24",
54
+ "@finos/legend-storage": "3.0.145",
55
55
  "@testing-library/dom": "10.4.0",
56
56
  "@testing-library/react": "16.2.0",
57
57
  "@types/react": "19.0.10",
@@ -367,7 +367,7 @@ export const QueryBuilderSidebar = observer(
367
367
  }) => {
368
368
  const { queryBuilderState, children } = props;
369
369
  const applicationStore = useApplicationStore();
370
- const extraTemplateQueryPanelContentRenderer =
370
+ const extraTemplateQueryPanelContentRenderers =
371
371
  applicationStore.pluginManager
372
372
  .getApplicationPlugins()
373
373
  .flatMap(
@@ -376,12 +376,15 @@ export const QueryBuilderSidebar = observer(
376
376
  plugin as QueryBuilder_LegendApplicationPlugin_Extension
377
377
  ).getExtraTemplateQueryPanelContentRenderer?.() ?? [],
378
378
  );
379
- const templateQueryPanelContentTab =
380
- extraTemplateQueryPanelContentRenderer[0] ? (
381
- extraTemplateQueryPanelContentRenderer[0](queryBuilderState)
382
- ) : (
383
- <></>
384
- );
379
+ // Try each renderer until one returns a non-undefined result.
380
+ let templateQueryPanelContentTab: React.ReactNode = <></>;
381
+ for (const renderer of extraTemplateQueryPanelContentRenderers) {
382
+ const result = renderer(queryBuilderState);
383
+ if (result !== undefined) {
384
+ templateQueryPanelContentTab = result;
385
+ break;
386
+ }
387
+ }
385
388
 
386
389
  return (
387
390
  <div
@@ -280,7 +280,17 @@ export const QueryBuilderTDSCellSelectionStatsBar = observer(
280
280
  <span className="query-builder__result__tds-grid__stats-bar__item__label">
281
281
  Count:
282
282
  </span>
283
- <span className="query-builder__result__tds-grid__stats-bar__item__value">
283
+ <span
284
+ className={clsx(
285
+ 'query-builder__result__tds-grid__stats-bar__item__value',
286
+ {
287
+ 'query-builder__result__tds-grid__stats-bar__item__value--with-chart':
288
+ !loading &&
289
+ stats.valueFrequencies !== undefined &&
290
+ stats.valueFrequencies.length > 0,
291
+ },
292
+ )}
293
+ >
284
294
  {countReady ? cellCount : <StatsSpinner />}
285
295
  </span>
286
296
  {!loading &&
@@ -305,7 +315,17 @@ export const QueryBuilderTDSCellSelectionStatsBar = observer(
305
315
  <span className="query-builder__result__tds-grid__stats-bar__item__label">
306
316
  Unique Count:
307
317
  </span>
308
- <span className="query-builder__result__tds-grid__stats-bar__item__value">
318
+ <span
319
+ className={clsx(
320
+ 'query-builder__result__tds-grid__stats-bar__item__value',
321
+ {
322
+ 'query-builder__result__tds-grid__stats-bar__item__value--with-chart':
323
+ !loading &&
324
+ stats.valueFrequencies !== undefined &&
325
+ stats.valueFrequencies.length > 0,
326
+ },
327
+ )}
328
+ >
309
329
  {loading ? <StatsSpinner /> : stats.uniqueCount}
310
330
  </span>
311
331
  {!loading &&
@@ -330,7 +350,17 @@ export const QueryBuilderTDSCellSelectionStatsBar = observer(
330
350
  <span className="query-builder__result__tds-grid__stats-bar__item__label">
331
351
  Empty Count:
332
352
  </span>
333
- <span className="query-builder__result__tds-grid__stats-bar__item__value">
353
+ <span
354
+ className={clsx(
355
+ 'query-builder__result__tds-grid__stats-bar__item__value',
356
+ {
357
+ 'query-builder__result__tds-grid__stats-bar__item__value--with-chart':
358
+ !loading &&
359
+ stats.valueFrequencies !== undefined &&
360
+ stats.valueFrequencies.length > 0,
361
+ },
362
+ )}
363
+ >
334
364
  {loading ? <StatsSpinner /> : stats.nullCount}
335
365
  </span>
336
366
  {!loading &&
@@ -26,6 +26,7 @@ import {
26
26
  SquareIcon,
27
27
  } from '@finos/legend-art';
28
28
  import { prettyDuration } from '@finos/legend-shared';
29
+ import { useApplicationStore } from '@finos/legend-application';
29
30
  import {
30
31
  PlaygroundSQLCodeEditor,
31
32
  type SQLPlaygroundPanelProps,
@@ -51,6 +52,10 @@ export const SQLPlaygroundEditorResultPanel = observer(
51
52
  accessorExplorerState,
52
53
  showAccessorExplorer = false,
53
54
  } = props;
55
+ const applicationStore = useApplicationStore();
56
+ const isGlobalDarkMode =
57
+ !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled;
58
+ const effectiveDarkMode = enableDarkMode || isGlobalDarkMode;
54
59
 
55
60
  const executeRawSQL = (): void => {
56
61
  playgroundState.executeRawSQL();
@@ -91,14 +96,14 @@ export const SQLPlaygroundEditorResultPanel = observer(
91
96
  playgroundState={playgroundState}
92
97
  advancedMode={advancedMode}
93
98
  disableDragDrop={disableDragDrop}
94
- enableDarkMode={enableDarkMode}
99
+ enableDarkMode={effectiveDarkMode}
95
100
  />
96
101
  </ResizablePanel>
97
102
  <ResizablePanelSplitter />
98
103
  <ResizablePanel size={300}>
99
104
  <div
100
105
  className={clsx('panel__header', {
101
- 'panel__header--dark': enableDarkMode,
106
+ 'panel__header--dark': effectiveDarkMode,
102
107
  })}
103
108
  >
104
109
  <div className="panel__header__title">
@@ -115,7 +120,7 @@ export const SQLPlaygroundEditorResultPanel = observer(
115
120
  (resultDescription ?? '')}
116
121
  </div>
117
122
  </div>
118
- <div className={!enableDarkMode ? 'light-mode' : ''}>
123
+ <div className={clsx({ 'light-mode': !effectiveDarkMode })}>
119
124
  <div className="panel__header__actions query-builder__result__header__actions">
120
125
  {advancedMode && (
121
126
  <div className="query-builder__result__advanced__mode">
@@ -166,7 +171,7 @@ export const SQLPlaygroundEditorResultPanel = observer(
166
171
  result={playgroundState.sqlExecutionResult.value}
167
172
  useAdvancedGrid={advancedMode}
168
173
  useLocalMode={playgroundState.isLocalModeEnabled}
169
- enableDarkMode={enableDarkMode}
174
+ enableDarkMode={effectiveDarkMode}
170
175
  />
171
176
  )}
172
177
  {playgroundState.sqlExecutionResult instanceof
@@ -177,7 +182,7 @@ export const SQLPlaygroundEditorResultPanel = observer(
177
182
  result={playgroundState.sqlExecutionResult.result}
178
183
  useAdvancedGrid={advancedMode}
179
184
  useLocalMode={playgroundState.isLocalModeEnabled}
180
- enableDarkMode={enableDarkMode}
185
+ enableDarkMode={effectiveDarkMode}
181
186
  />
182
187
  )}
183
188
  </ResizablePanel>
@@ -32,6 +32,7 @@ import {
32
32
  SimpleFunctionExpression,
33
33
  InstanceValue,
34
34
  Multiplicity,
35
+ type MappingModelCoverageAnalysisResult,
35
36
  extractElementNameFromPath,
36
37
  SUPPORTED_FUNCTIONS,
37
38
  PackageableElementExplicitReference,
@@ -53,6 +54,8 @@ import {
53
54
  GenericTypeExplicitReference,
54
55
  findLakehouseAccessPointGroup,
55
56
  type PureModel,
57
+ DataProductAccessType,
58
+ LegendSDLC,
56
59
  } from '@finos/legend-graph';
57
60
  import { QueryBuilderState } from '../../QueryBuilderState.js';
58
61
 
@@ -74,6 +77,7 @@ import { action, computed, flow, makeObservable, observable } from 'mobx';
74
77
  import {
75
78
  DepotEntityWithOrigin,
76
79
  type QueryableSourceInfo,
80
+ type ProjectGAVCoordinates,
77
81
  } from '@finos/legend-storage';
78
82
  import { compareLabelFn } from '@finos/legend-art';
79
83
  import { QueryBuilderEmbeddedFromExecutionContextState } from '../../QueryBuilderExecutionContextState.js';
@@ -303,9 +307,6 @@ export class LakehouseDataProductExecutionState extends DataProductExecutionStat
303
307
  export class DataProductQueryBuilderState extends QueryBuilderState {
304
308
  readonly onClassChange?: ((val: Class) => void) | undefined;
305
309
  readonly onDataProductChange?: (val: DepotEntityWithOrigin) => Promise<void>;
306
- readonly onExecutionContextChange?:
307
- | ((val: NativeModelExecutionContext) => void)
308
- | undefined;
309
310
 
310
311
  loadDataProductModelState = ActionState.create();
311
312
  dataProduct: DataProduct;
@@ -314,6 +315,10 @@ export class DataProductQueryBuilderState extends QueryBuilderState {
314
315
  NativeModelExecutionContext | ModelAccessPointGroup | LakehouseAccessPoint
315
316
  >;
316
317
  entities: DepotEntityWithOrigin[] | undefined;
318
+ mappingToMappingCoverageResult?: Map<
319
+ string,
320
+ MappingModelCoverageAnalysisResult
321
+ >;
317
322
 
318
323
  prioritizeEntityFunc?: ((val: DepotEntityWithOrigin) => boolean) | undefined;
319
324
 
@@ -333,9 +338,6 @@ export class DataProductQueryBuilderState extends QueryBuilderState {
333
338
  | LakehouseAccessPoint,
334
339
  prioritizeEntityFunc: ((val: DepotEntityWithOrigin) => boolean) | undefined,
335
340
  onDataProductChange: (val: DepotEntityWithOrigin) => Promise<void>,
336
- onExecutionContextChange?:
337
- | ((val: NativeModelExecutionContext) => void)
338
- | undefined,
339
341
  onClassChange?: ((val: Class) => void) | undefined,
340
342
  config?: QueryBuilderConfig | undefined,
341
343
  sourceInfo?: QueryableSourceInfo | undefined,
@@ -357,6 +359,11 @@ export class DataProductQueryBuilderState extends QueryBuilderState {
357
359
  showExecutionIdSelector: computed,
358
360
  executionIdOptions: computed,
359
361
  selectedExecutionIdOption: computed,
362
+ hasNativeModelAccess: computed,
363
+ hasBothAccessModes: computed,
364
+ showContextSelector: computed,
365
+ allContextOptions: computed,
366
+ selectedContextOption: computed,
360
367
  selectedExecOption: computed,
361
368
  selectedModelAccessPointGroupOption: computed,
362
369
  usableClasses: computed,
@@ -382,13 +389,33 @@ export class DataProductQueryBuilderState extends QueryBuilderState {
382
389
  : new ModelAccessPointDataProductExecutionState(executionState, this);
383
390
  this.prioritizeEntityFunc = prioritizeEntityFunc;
384
391
  this.onDataProductChange = onDataProductChange;
385
- this.onExecutionContextChange = onExecutionContextChange;
386
392
  this.onClassChange = onClassChange;
387
393
  // force from.
388
394
  this.executionContextState =
389
395
  new QueryBuilderEmbeddedFromExecutionContextState(this);
390
396
  }
391
397
 
398
+ async prepareAccessForExecution(): Promise<void> {
399
+ if (this.executionState instanceof NativeModelDataProductExecutionState) {
400
+ const runtime = this.executionState.exectionValue.runtime;
401
+ if (runtime) {
402
+ this.changeRuntime(new RuntimePointer(runtime));
403
+ }
404
+ } else if (
405
+ this.executionState instanceof
406
+ ModelAccessPointDataProductExecutionState &&
407
+ this.executionState.selectedRuntime
408
+ ) {
409
+ this.changeRuntime(
410
+ new RuntimePointer(
411
+ PackageableElementExplicitReference.create(
412
+ this.executionState.selectedRuntime,
413
+ ),
414
+ ),
415
+ );
416
+ }
417
+ }
418
+
392
419
  get isProductLinkable(): boolean {
393
420
  return false;
394
421
  }
@@ -417,19 +444,40 @@ export class DataProductQueryBuilderState extends QueryBuilderState {
417
444
  this.modelAccessPointGroupOptions.length > 1
418
445
  );
419
446
  }
447
+ get hasNativeModelAccess(): boolean {
448
+ return this.dataProduct.nativeModelAccess !== undefined;
449
+ }
450
+
451
+ get hasBothAccessModes(): boolean {
452
+ return this.hasModelAccessPointGroups && this.hasNativeModelAccess;
453
+ }
454
+
455
+ // showContextSelector / allContextOptions / selectedContextOption are aliases
456
+ // over the upstream ExecutionIdOption API so existing UI code keeps working.
457
+ get showContextSelector(): boolean {
458
+ return this.showExecutionIdSelector;
459
+ }
460
+
461
+ get allContextOptions(): ExecutionIdOption[] {
462
+ return this.executionIdOptions;
463
+ }
464
+
465
+ get selectedContextOption(): ExecutionIdOption | undefined {
466
+ return this.selectedExecutionIdOption;
467
+ }
420
468
 
421
469
  get executionIdOptions(): ExecutionIdOption[] {
422
470
  const nativeOptions: ExecutionIdOption[] = (
423
471
  this.dataProduct.nativeModelAccess?.nativeModelExecutionContexts ?? []
424
472
  ).map((ctx) => ({
425
473
  label: ctx.key,
426
- tag: 'Native',
474
+ tag: 'NATIVE',
427
475
  value: ctx,
428
476
  }));
429
477
  const modelOptions: ExecutionIdOption[] = this.modelAccessPointGroups.map(
430
478
  (group) => ({
431
479
  label: group.title ?? group.id,
432
- tag: 'Model',
480
+ tag: 'MODEL',
433
481
  value: group,
434
482
  }),
435
483
  );
@@ -439,7 +487,7 @@ export class DataProductQueryBuilderState extends QueryBuilderState {
439
487
  .filter(filterByType(LakehouseAccessPoint))
440
488
  .map((ap) => ({
441
489
  label: ap.title ?? ap.id,
442
- tag: 'Lakehouse',
490
+ tag: 'LAKEHOUSE',
443
491
  value: ap,
444
492
  }));
445
493
  return [...modelOptions, ...lakehouseOptions, ...nativeOptions].sort(
@@ -452,19 +500,19 @@ export class DataProductQueryBuilderState extends QueryBuilderState {
452
500
  if (state instanceof NativeModelDataProductExecutionState) {
453
501
  return {
454
502
  label: state.exectionValue.key,
455
- tag: 'Native',
503
+ tag: 'NATIVE',
456
504
  value: state.exectionValue,
457
505
  };
458
506
  } else if (state instanceof ModelAccessPointDataProductExecutionState) {
459
507
  return {
460
508
  label: state.exectionValue.title ?? state.exectionValue.id,
461
- tag: 'Model',
509
+ tag: 'MODEL',
462
510
  value: state.exectionValue,
463
511
  };
464
512
  } else if (state instanceof LakehouseDataProductExecutionState) {
465
513
  return {
466
514
  label: state.exectionValue.title ?? state.exectionValue.id,
467
- tag: 'Lakehouse',
515
+ tag: 'LAKEHOUSE',
468
516
  value: state.exectionValue,
469
517
  };
470
518
  }
@@ -480,11 +528,20 @@ export class DataProductQueryBuilderState extends QueryBuilderState {
480
528
  if (val === this.executionState.exectionValue) {
481
529
  return;
482
530
  }
531
+ const switchingToModel = val instanceof ModelAccessPointGroup;
532
+ const switchingToNative = val instanceof NativeModelExecutionContext;
533
+ const wasModeSwitch =
534
+ (switchingToModel && this.isNativeMode) ||
535
+ (switchingToNative && this.isModelAccessPointGroupMode);
536
+
537
+ if (wasModeSwitch) {
538
+ this.changeHistoryState.querySnapshotBuffer = [];
539
+ this.changeHistoryState.pointer = -1;
540
+ this.changeHistoryState.setCurrentQuery(undefined);
541
+ }
542
+
483
543
  await this.changeExecutionState(val);
484
544
  await this.propagateExecutionContextChange();
485
- if (val instanceof NativeModelExecutionContext) {
486
- this.onExecutionContextChange?.(val);
487
- }
488
545
  }
489
546
 
490
547
  get selectedExecOption():
@@ -523,29 +580,6 @@ export class DataProductQueryBuilderState extends QueryBuilderState {
523
580
  : [];
524
581
  }
525
582
 
526
- changeNativeExecutionContext(val: NativeModelExecutionContext): void {
527
- if (this.isNativeMode && val === this.executionState.exectionValue) {
528
- return;
529
- }
530
- this.setExecutionState(val);
531
- this.propagateExecutionContextChange()
532
- .then(() => this.onExecutionContextChange?.(val))
533
- .catch(this.applicationStore.alertUnhandledError);
534
- }
535
-
536
- changeModelAccessPointGroupValue(val: ModelAccessPointGroup): void {
537
- if (
538
- this.isModelAccessPointGroupMode &&
539
- val === this.executionState.exectionValue
540
- ) {
541
- return;
542
- }
543
- this.setExecutionState(val);
544
- this.propagateExecutionContextChange().catch(
545
- this.applicationStore.alertUnhandledError,
546
- );
547
- }
548
-
549
583
  override buildQueryForPersistence(): RawLambda {
550
584
  if (!this.isQuerySupported) {
551
585
  return this.buildQuery();
@@ -632,12 +666,7 @@ export class DataProductQueryBuilderState extends QueryBuilderState {
632
666
  const execValue =
633
667
  preResolvedState ?? resolveDataProductExecutionState(dataProduct);
634
668
  this.dataProduct = dataProduct;
635
- this.executionState =
636
- execValue instanceof NativeModelExecutionContext
637
- ? new NativeModelDataProductExecutionState(execValue, this)
638
- : execValue instanceof LakehouseAccessPoint
639
- ? new LakehouseDataProductExecutionState(execValue, this)
640
- : new ModelAccessPointDataProductExecutionState(execValue, this);
669
+ this.setExecutionState(execValue);
641
670
  const mapping = this.executionState.mapping;
642
671
  if (mapping) {
643
672
  this.changeMapping(mapping);
@@ -669,11 +698,18 @@ export class DataProductQueryBuilderState extends QueryBuilderState {
669
698
  );
670
699
  }
671
700
  if (mapping) {
701
+ const coverageResult = this.mappingToMappingCoverageResult?.get(
702
+ mapping.path,
703
+ );
704
+ if (coverageResult) {
705
+ this.explorerState.mappingModelCoverageAnalysisResult =
706
+ coverageResult;
707
+ }
672
708
  const compatibleClasses = resolveUsableDataProductClasses(
673
709
  this.activeFeaturedElements,
674
710
  mapping,
675
711
  this.graphManagerState,
676
- undefined,
712
+ coverageResult,
677
713
  );
678
714
  // if there is no chosen class or the chosen one is not compatible
679
715
  // with the mapping then pick a compatible class if possible
@@ -882,19 +918,87 @@ export class DataProductQueryBuilderState extends QueryBuilderState {
882
918
  ): Promise<void> {
883
919
  const currentMapping = this.executionContextState.mapping;
884
920
  const newMapping = this.activeMapping;
885
- if (newMapping && newMapping !== currentMapping) {
886
- this.changeMapping(newMapping, {
887
- keepQueryContent: true,
888
- });
889
- const classes = resolveUsableDataProductClasses(
890
- this.activeFeaturedElements,
891
- newMapping,
892
- this.graphManagerState,
893
- undefined,
894
- );
895
- if (this.sourceClass && !classes.includes(this.sourceClass)) {
896
- this.setSourceElement(classes[0]);
921
+ if (!newMapping || newMapping === currentMapping) {
922
+ return;
923
+ }
924
+
925
+ const coverageResult = this.mappingToMappingCoverageResult?.get(
926
+ newMapping.path,
927
+ );
928
+
929
+ if (coverageResult && this.dataProductArtifact) {
930
+ const origin = this.graphManagerState.graph.origin;
931
+ if (origin instanceof LegendSDLC) {
932
+ const newGraph = this.graphManagerState.createNewGraph();
933
+ const projectInfo: ProjectGAVCoordinates = {
934
+ groupId: origin.groupId,
935
+ artifactId: origin.artifactId,
936
+ versionId: origin.versionId,
937
+ };
938
+ let accessPointId: string;
939
+ let dataProductAccessType: DataProductAccessType;
940
+ if (
941
+ this.executionState instanceof NativeModelDataProductExecutionState
942
+ ) {
943
+ accessPointId = this.executionState.exectionValue.key;
944
+ dataProductAccessType = DataProductAccessType.NATIVE;
945
+ } else {
946
+ accessPointId = (
947
+ this.executionState as ModelAccessPointDataProductExecutionState
948
+ ).exectionValue.id;
949
+ dataProductAccessType = DataProductAccessType.MODEL;
950
+ }
951
+ const analysisResult =
952
+ await this.graphManagerState.graphManager.buildDataProductAnalysis(
953
+ this.dataProductArtifact,
954
+ this.dataProduct.path,
955
+ newGraph,
956
+ accessPointId,
957
+ dataProductAccessType,
958
+ projectInfo,
959
+ );
960
+ this.graphManagerState.graph = newGraph;
961
+
962
+ this.dataProduct = newGraph.getDataProduct(this.dataProduct.path);
963
+ this.setExecutionState(analysisResult.targetExecState);
964
+ if (analysisResult.dataProductAnalysis.mappingToMappingCoverageResult) {
965
+ this.mappingToMappingCoverageResult =
966
+ analysisResult.dataProductAnalysis.mappingToMappingCoverageResult;
967
+ }
968
+ const newCoverageResult = this.mappingToMappingCoverageResult?.get(
969
+ newMapping.path,
970
+ );
971
+ if (newCoverageResult) {
972
+ this.explorerState.mappingModelCoverageAnalysisResult =
973
+ newCoverageResult;
974
+ }
975
+ } else {
976
+ this.explorerState.mappingModelCoverageAnalysisResult = coverageResult;
977
+ }
978
+ } else if (coverageResult) {
979
+ this.explorerState.mappingModelCoverageAnalysisResult = coverageResult;
980
+ }
981
+
982
+ await this.prepareAccessForExecution();
983
+
984
+ this.changeMapping(newMapping, {
985
+ keepQueryContent: true,
986
+ });
987
+ const classes = resolveUsableDataProductClasses(
988
+ this.activeFeaturedElements,
989
+ newMapping,
990
+ this.graphManagerState,
991
+ this.explorerState.mappingModelCoverageAnalysisResult,
992
+ );
993
+ if (
994
+ !this.sourceClass ||
995
+ (!classes.includes(this.sourceClass) && classes.length)
996
+ ) {
997
+ const possibleNewClass = classes[0];
998
+ if (possibleNewClass) {
999
+ this.changeSourceElement(possibleNewClass);
897
1000
  }
898
1001
  }
1002
+ this.explorerState.refreshTreeData();
899
1003
  }
900
1004
  }