@finos/legend-query-builder 4.10.2 → 4.11.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -37,6 +37,8 @@ import {
37
37
  ModalFooterButton,
38
38
  ModalHeader,
39
39
  PanelDivider,
40
+ SquareIcon,
41
+ CheckSquareIcon,
40
42
  } from '@finos/legend-art';
41
43
  import { format as formatSQL } from 'sql-formatter';
42
44
  import { observer } from 'mobx-react-lite';
@@ -53,6 +55,7 @@ import {
53
55
  EnumValueExplicitReference,
54
56
  RelationalExecutionActivities,
55
57
  getTDSRowRankByColumnInAsc,
58
+ PRIMITIVE_TYPE,
56
59
  } from '@finos/legend-graph';
57
60
  import {
58
61
  ActionAlertActionType,
@@ -100,6 +103,7 @@ import { PARAMETER_SUBMIT_ACTION } from '../stores/shared/LambdaParameterState.j
100
103
  import { QUERY_BUILDER_TEST_ID } from '../__lib__/QueryBuilderTesting.js';
101
104
  import {
102
105
  DataGrid,
106
+ type DataGridColumnApi,
103
107
  type DataGridCellRendererParams,
104
108
  type DataGridColumnDefinition,
105
109
  } from '@finos/legend-lego/data-grid';
@@ -119,6 +123,8 @@ import {
119
123
  } from '../stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_IsEmpty.js';
120
124
  import { QueryUsageViewer } from './QueryUsageViewer.js';
121
125
  import { DEFAULT_LOCALE } from '../graph-manager/QueryBuilderConst.js';
126
+ import { DocumentationLink } from '@finos/legend-lego/application';
127
+ import { QUERY_BUILDER_DOCUMENTATION_KEY } from '../__lib__/QueryBuilderDocumentation.js';
122
128
 
123
129
  export const tryToFormatSql = (sql: string): string => {
124
130
  try {
@@ -732,6 +738,40 @@ const QueryResultCellRenderer = observer(
732
738
  },
733
739
  );
734
740
 
741
+ const getColumnCustomizations = (
742
+ result: TDSExecutionResult,
743
+ columnName: string,
744
+ ): object | undefined => {
745
+ const columnType = result.builder.columns.find(
746
+ (col) => col.name === columnName,
747
+ )?.type;
748
+ switch (columnType) {
749
+ case PRIMITIVE_TYPE.STRING:
750
+ return {
751
+ filter: 'agTextColumnFilter',
752
+ allowedAggFuncs: ['count'],
753
+ };
754
+ case PRIMITIVE_TYPE.DATE:
755
+ case PRIMITIVE_TYPE.DATETIME:
756
+ case PRIMITIVE_TYPE.STRICTDATE:
757
+ return {
758
+ filter: 'agDateColumnFilter',
759
+ allowedAggFuncs: ['count'],
760
+ };
761
+ case PRIMITIVE_TYPE.DECIMAL:
762
+ case PRIMITIVE_TYPE.INTEGER:
763
+ case PRIMITIVE_TYPE.FLOAT:
764
+ return {
765
+ filter: 'agNumberColumnFilter',
766
+ allowedAggFuncs: ['count', 'sum', 'max', 'min', 'avg'],
767
+ };
768
+ default:
769
+ return {
770
+ allowedAggFuncs: ['count'],
771
+ };
772
+ }
773
+ };
774
+
735
775
  const QueryBuilderGridResult = observer(
736
776
  (props: {
737
777
  executionResult: TDSExecutionResult;
@@ -739,7 +779,56 @@ const QueryBuilderGridResult = observer(
739
779
  }) => {
740
780
  const { executionResult, queryBuilderState } = props;
741
781
 
782
+ const [columnAPi, setColumnApi] = useState<DataGridColumnApi | undefined>(
783
+ undefined,
784
+ );
742
785
  const resultState = queryBuilderState.resultState;
786
+ const isAdvancedModeEnabled = queryBuilderState.isAdvancedModeEnabled;
787
+ const colDefs = isAdvancedModeEnabled
788
+ ? executionResult.result.columns.map((colName) => {
789
+ const col = {
790
+ minWidth: 50,
791
+ sortable: true,
792
+ resizable: true,
793
+ field: colName,
794
+ flex: 1,
795
+ enablePivot: true,
796
+ enableRowGroup: true,
797
+ enableValue: true,
798
+ ...getColumnCustomizations(executionResult, colName),
799
+ } as DataGridColumnDefinition;
800
+ const persistedColumn = resultState.gridConfig.columns.find(
801
+ (c) => c.colId === colName,
802
+ );
803
+ if (persistedColumn) {
804
+ if (persistedColumn.width) {
805
+ col.width = persistedColumn.width;
806
+ }
807
+ col.pinned = persistedColumn.pinned ?? null;
808
+ col.rowGroup = persistedColumn.rowGroup ?? false;
809
+ col.rowGroupIndex = persistedColumn.rowGroupIndex ?? null;
810
+ col.aggFunc = persistedColumn.aggFunc ?? null;
811
+ col.pivot = persistedColumn.pivot ?? false;
812
+ col.hide = persistedColumn.hide ?? false;
813
+ }
814
+ return col;
815
+ })
816
+ : executionResult.result.columns.map(
817
+ (colName) =>
818
+ ({
819
+ minWidth: 50,
820
+ sortable: true,
821
+ resizable: true,
822
+ field: colName,
823
+ flex: 1,
824
+ cellRenderer: QueryResultCellRenderer,
825
+ cellRendererParams: {
826
+ resultState: resultState,
827
+ tdsExecutionResult: executionResult,
828
+ },
829
+ }) as DataGridColumnDefinition,
830
+ );
831
+ const sideBar = isAdvancedModeEnabled ? ['columns', 'filters'] : null;
743
832
 
744
833
  const rowData = executionResult.result.rows.map((_row, rowIdx) => {
745
834
  const row: PlainObject = {};
@@ -754,6 +843,15 @@ const QueryBuilderGridResult = observer(
754
843
  row.rowNumber = rowIdx;
755
844
  return row;
756
845
  });
846
+ const onSaveGridColumnState = (): void => {
847
+ if (!columnAPi) {
848
+ return;
849
+ }
850
+ resultState.setGridConfig({
851
+ columns: columnAPi.getColumnState(),
852
+ isPivotModeEnabled: columnAPi.isPivotMode(),
853
+ });
854
+ };
757
855
 
758
856
  return (
759
857
  <div className="query-builder__result__values__table">
@@ -762,34 +860,57 @@ const QueryBuilderGridResult = observer(
762
860
  'ag-theme-balham-dark query-builder__result__tds-grid',
763
861
  )}
764
862
  >
765
- <DataGrid
766
- rowData={rowData}
767
- gridOptions={{
768
- suppressScrollOnNewData: true,
769
- getRowId: (data) => data.data.rowNumber,
770
- }}
771
- // NOTE: when column definition changed, we need to force refresh the cell to make sure the cell renderer is updated
772
- // See https://stackoverflow.com/questions/56341073/how-to-refresh-an-ag-grid-when-a-change-occurs-inside-a-custom-cell-renderer-com
773
- onRowDataUpdated={(params) => {
774
- params.api.refreshCells({ force: true });
775
- }}
776
- suppressFieldDotNotation={true}
777
- columnDefs={executionResult.result.columns.map(
778
- (colName) =>
779
- ({
780
- minWidth: 50,
781
- sortable: true,
782
- resizable: true,
783
- field: colName,
784
- flex: 1,
785
- cellRenderer: QueryResultCellRenderer,
786
- cellRendererParams: {
787
- resultState: resultState,
788
- tdsExecutionResult: executionResult,
789
- },
790
- }) as DataGridColumnDefinition,
791
- )}
792
- />
863
+ {isAdvancedModeEnabled ? (
864
+ <DataGrid
865
+ rowData={rowData}
866
+ onGridReady={(params): void => {
867
+ setColumnApi(params.columnApi);
868
+ params.columnApi.setPivotMode(
869
+ resultState.gridConfig.isPivotModeEnabled,
870
+ );
871
+ }}
872
+ gridOptions={{
873
+ suppressScrollOnNewData: true,
874
+ getRowId: (data) => data.data.rowNumber,
875
+ rowSelection: 'multiple',
876
+ pivotPanelShow: 'always',
877
+ rowGroupPanelShow: 'always',
878
+ }}
879
+ // NOTE: when column definition changed, we need to force refresh the cell to make sure the cell renderer is updated
880
+ // See https://stackoverflow.com/questions/56341073/how-to-refresh-an-ag-grid-when-a-change-occurs-inside-a-custom-cell-renderer-com
881
+ onRowDataUpdated={(params) => {
882
+ params.api.refreshCells({ force: true });
883
+ }}
884
+ suppressFieldDotNotation={true}
885
+ suppressContextMenu={!isAdvancedModeEnabled}
886
+ columnDefs={colDefs}
887
+ sideBar={sideBar}
888
+ onColumnVisible={onSaveGridColumnState}
889
+ onColumnPinned={onSaveGridColumnState}
890
+ onColumnResized={onSaveGridColumnState}
891
+ onColumnRowGroupChanged={onSaveGridColumnState}
892
+ onColumnValueChanged={onSaveGridColumnState}
893
+ onColumnPivotChanged={onSaveGridColumnState}
894
+ onColumnPivotModeChanged={onSaveGridColumnState}
895
+ />
896
+ ) : (
897
+ <DataGrid
898
+ rowData={rowData}
899
+ gridOptions={{
900
+ suppressScrollOnNewData: true,
901
+ getRowId: (data) => data.data.rowNumber,
902
+ rowSelection: 'multiple',
903
+ }}
904
+ // NOTE: when column definition changed, we need to force refresh the cell to make sure the cell renderer is updated
905
+ // See https://stackoverflow.com/questions/56341073/how-to-refresh-an-ag-grid-when-a-change-occurs-inside-a-custom-cell-renderer-com
906
+ onRowDataUpdated={(params) => {
907
+ params.api.refreshCells({ force: true });
908
+ }}
909
+ suppressFieldDotNotation={true}
910
+ suppressContextMenu={!isAdvancedModeEnabled}
911
+ columnDefs={colDefs}
912
+ />
913
+ )}
793
914
  </div>
794
915
  </div>
795
916
  );
@@ -906,6 +1027,11 @@ export const QueryBuilderResultPanel = observer(
906
1027
  const isQueryValid =
907
1028
  !queryBuilderState.isQuerySupported || isSupportedQueryValid;
908
1029
 
1030
+ const isQueryValidForAdvancedMode =
1031
+ isQueryValid &&
1032
+ queryBuilderState.fetchStructureState.implementation instanceof
1033
+ QueryBuilderTDSState;
1034
+
909
1035
  const runQuery = (): void => {
910
1036
  resultState.setSelectedCells([]);
911
1037
  resultState.pressedRunQuery.inProgress();
@@ -936,6 +1062,7 @@ export const QueryBuilderResultPanel = observer(
936
1062
  );
937
1063
 
938
1064
  const allowSettingPreviewLimit = queryBuilderState.isQuerySupported;
1065
+ const allowSettingAdvancedMode = queryBuilderState.isQuerySupported;
939
1066
 
940
1067
  const copyExpression = (value: string): void => {
941
1068
  applicationStore.clipboardService
@@ -1008,6 +1135,13 @@ export const QueryBuilderResultPanel = observer(
1008
1135
  }
1009
1136
  };
1010
1137
 
1138
+ const toggleIsAdvancedModeEnabled = (): void => {
1139
+ resultState.setExecutionResult(undefined);
1140
+ queryBuilderState.setIsAdvancedModeEnabled(
1141
+ !queryBuilderState.isAdvancedModeEnabled,
1142
+ );
1143
+ };
1144
+
1011
1145
  return (
1012
1146
  <div
1013
1147
  data-testid={QUERY_BUILDER_TEST_ID.QUERY_BUILDER_RESULT_PANEL}
@@ -1084,6 +1218,38 @@ export const QueryBuilderResultPanel = observer(
1084
1218
  )}
1085
1219
  </div>
1086
1220
  <div className="panel__header__actions query-builder__result__header__actions">
1221
+ {allowSettingAdvancedMode && (
1222
+ <div className="query-builder__result__advanced__mode">
1223
+ <div className="query-builder__result__advanced__mode__label">
1224
+ Advanced Mode
1225
+ <DocumentationLink
1226
+ title="The grid in advanced mode performs all operations like grouping, sorting, filtering, etc after initial query execution locally withought reaching out to server. This limits the number of rows to smaller number so they can fit in memory"
1227
+ documentationKey={
1228
+ QUERY_BUILDER_DOCUMENTATION_KEY.QUESTION_HOW_TO_USE_ADVANCED_GRID_MODE
1229
+ }
1230
+ />
1231
+ </div>
1232
+ <button
1233
+ className={clsx(
1234
+ 'query-builder__result__advanced__mode__toggler__btn',
1235
+ {
1236
+ 'query-builder__result__advanced__mode__toggler__btn--toggled':
1237
+ queryBuilderState.isAdvancedModeEnabled,
1238
+ },
1239
+ )}
1240
+ disabled={!isQueryValidForAdvancedMode}
1241
+ onClick={toggleIsAdvancedModeEnabled}
1242
+ tabIndex={-1}
1243
+ >
1244
+ {queryBuilderState.isAdvancedModeEnabled ? (
1245
+ <CheckSquareIcon />
1246
+ ) : (
1247
+ <SquareIcon />
1248
+ )}
1249
+ </button>
1250
+ </div>
1251
+ )}
1252
+
1087
1253
  {allowSettingPreviewLimit && (
1088
1254
  <div className="query-builder__result__limit">
1089
1255
  <div className="query-builder__result__limit__label">
@@ -48,6 +48,7 @@ import type { LambdaFunctionBuilderOption } from './QueryBuilderValueSpecificati
48
48
  import { QueryBuilderTelemetryHelper } from '../__lib__/QueryBuilderTelemetryHelper.js';
49
49
  import { QUERY_BUILDER_EVENT } from '../__lib__/QueryBuilderEvent.js';
50
50
  import { ExecutionPlanState } from './execution-plan/ExecutionPlanState.js';
51
+ import type { DataGridColumnState } from '@finos/legend-lego/data-grid';
51
52
 
52
53
  export const DEFAULT_LIMIT = 1000;
53
54
 
@@ -67,6 +68,11 @@ export interface QueryBuilderTDSResultCellCoordinate {
67
68
  colIndex: number;
68
69
  }
69
70
 
71
+ type QueryBuilderDataGridConfig = {
72
+ columns: DataGridColumnState[];
73
+ isPivotModeEnabled: boolean;
74
+ };
75
+
70
76
  export class QueryBuilderResultState {
71
77
  readonly queryBuilderState: QueryBuilderState;
72
78
  readonly exportDataState = ActionState.create();
@@ -86,6 +92,8 @@ export class QueryBuilderResultState {
86
92
  mousedOverCell: QueryBuilderTDSResultCellData | null = null;
87
93
  isSelectingCells: boolean;
88
94
 
95
+ gridConfig!: QueryBuilderDataGridConfig;
96
+
89
97
  constructor(queryBuilderState: QueryBuilderState) {
90
98
  makeObservable(this, {
91
99
  executionResult: observable,
@@ -99,6 +107,8 @@ export class QueryBuilderResultState {
99
107
  isRunningQuery: observable,
100
108
  isSelectingCells: observable,
101
109
  isQueryUsageViewerOpened: observable,
110
+ gridConfig: observable,
111
+ setGridConfig: action,
102
112
  setIsSelectingCells: action,
103
113
  setIsRunningQuery: action,
104
114
  setExecutionResult: action,
@@ -117,6 +127,10 @@ export class QueryBuilderResultState {
117
127
  this.isSelectingCells = false;
118
128
 
119
129
  this.selectedCells = [];
130
+ this.gridConfig = {
131
+ columns: [],
132
+ isPivotModeEnabled: false,
133
+ };
120
134
  this.queryBuilderState = queryBuilderState;
121
135
  this.executionPlanState = new ExecutionPlanState(
122
136
  this.queryBuilderState.applicationStore,
@@ -124,6 +138,10 @@ export class QueryBuilderResultState {
124
138
  );
125
139
  }
126
140
 
141
+ setGridConfig(val: QueryBuilderDataGridConfig): void {
142
+ this.gridConfig = val;
143
+ }
144
+
127
145
  setIsSelectingCells(val: boolean): void {
128
146
  this.isSelectingCells = val;
129
147
  }
@@ -131,6 +131,7 @@ export abstract class QueryBuilderState implements CommandRegistrar {
131
131
  isCheckingEntitlments = false;
132
132
  isCalendarEnabled = false;
133
133
  isQueryChatOpened = false;
134
+ isAdvancedModeEnabled = false;
134
135
 
135
136
  class?: Class | undefined;
136
137
  executionContextState: QueryBuilderExecutionContextState;
@@ -166,6 +167,7 @@ export abstract class QueryBuilderState implements CommandRegistrar {
166
167
  executionContextState: observable,
167
168
  class: observable,
168
169
  isQueryChatOpened: observable,
170
+ isAdvancedModeEnabled: observable,
169
171
 
170
172
  sideBarClassName: computed,
171
173
  isQuerySupported: computed,
@@ -178,6 +180,7 @@ export abstract class QueryBuilderState implements CommandRegistrar {
178
180
  setIsCheckingEntitlments: action,
179
181
  setClass: action,
180
182
  setIsQueryChatOpened: action,
183
+ setIsAdvancedModeEnabled: action,
181
184
 
182
185
  resetQueryResult: action,
183
186
  resetQueryContent: action,
@@ -260,6 +263,10 @@ export abstract class QueryBuilderState implements CommandRegistrar {
260
263
  this.isQueryChatOpened = val;
261
264
  }
262
265
 
266
+ setIsAdvancedModeEnabled(val: boolean): void {
267
+ this.isAdvancedModeEnabled = val;
268
+ }
269
+
263
270
  setInternalize(val: QueryBuilderInternalizeState | undefined): void {
264
271
  this.internalizeState = val;
265
272
  }