@quillsql/react 1.5.3 → 1.5.5

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.
@@ -0,0 +1,2208 @@
1
+ import React, {
2
+ useState,
3
+ useContext,
4
+ useCallback,
5
+ useEffect,
6
+ useRef,
7
+ } from 'react';
8
+ // import './nightOwlLight.css';
9
+ import axios from 'axios';
10
+ import { TailSpin } from 'react-loader-spinner';
11
+ import { ClientContext, SchemaContext, ThemeContext } from './Context';
12
+ import {
13
+ ChevronDownIcon,
14
+ ChevronRightIcon,
15
+ SparklesIcon,
16
+ MagnifyingGlassIcon,
17
+ XMarkIcon,
18
+ CheckIcon,
19
+ } from '@heroicons/react/20/solid';
20
+ import { convertPostgresColumn } from './SQLEditor';
21
+ import { format } from 'date-fns';
22
+
23
+ interface Option {
24
+ value: string;
25
+ label: string;
26
+ }
27
+
28
+ interface SelectComponentProps {
29
+ onChange: (value: string) => void;
30
+ value: string;
31
+ options: Option[];
32
+ }
33
+
34
+ interface ReportBuilderProps {
35
+ onChangeQuery: (query: string) => void;
36
+ onChangeData: (data: object[]) => void;
37
+ onChangeColumns: (columns: object[]) => void;
38
+ onChangeLoading: (loading: boolean) => void;
39
+ onError: (error: string) => void;
40
+ SelectComponent: (props: SelectComponentProps) => JSX.Element;
41
+ }
42
+
43
+ export default function ReportBuilder({
44
+ onChangeQuery,
45
+ onChangeData,
46
+ onChangeColumns,
47
+ onChangeLoading,
48
+ onError,
49
+ SelectComponent,
50
+ }: ReportBuilderProps) {
51
+ const [data, setData] = useState([]);
52
+ const [client] = useContext(ClientContext);
53
+ const [schema, setSchema] = useContext(SchemaContext);
54
+ const [theme] = useContext(ThemeContext);
55
+ const [columns, setColumns] = useState([]);
56
+ const [fields, setFields] = useState([]);
57
+
58
+ useEffect(() => {
59
+ async function getData(schema) {
60
+ if (schema.length && !data.length) {
61
+ const { publicKey, customerId, environment } = client;
62
+ const response = await axios.post(
63
+ `https://quill-344421.uc.r.appspot.com/dashquery`,
64
+ {
65
+ query: `select * from ${schema[0].displayName} limit 10;`,
66
+ },
67
+ {
68
+ params: {
69
+ orgId: customerId,
70
+ publicKey,
71
+ },
72
+ headers: {
73
+ Authorization: `Bearer `,
74
+ environment: environment || undefined,
75
+ },
76
+ }
77
+ );
78
+ setData(response.data.rows);
79
+ }
80
+ }
81
+ getData(schema);
82
+ }, [schema]);
83
+
84
+ useEffect(() => {
85
+ let isSubscribed = true;
86
+ async function getSchema() {
87
+ const { publicKey, environment } = client;
88
+ const response3 = await axios.get(
89
+ `https://quill-344421.uc.r.appspot.com/schema2/${publicKey}/`,
90
+ {
91
+ headers: {
92
+ Authorization: `Bearer `,
93
+ environment: environment || undefined,
94
+ },
95
+ }
96
+ );
97
+ if (isSubscribed) {
98
+ setSchema(response3.data.tables);
99
+ }
100
+ }
101
+ if (isSubscribed) {
102
+ getSchema();
103
+ }
104
+ return () => {
105
+ isSubscribed = false;
106
+ };
107
+ }, []);
108
+
109
+ const runQuery = async query => {
110
+ const { publicKey, customerId, environment } = client;
111
+ const response = await axios.post(
112
+ `https://quill-344421.uc.r.appspot.com/dashquery`,
113
+ {
114
+ query,
115
+ },
116
+ {
117
+ params: {
118
+ orgId: customerId,
119
+ publicKey,
120
+ },
121
+ headers: {
122
+ Authorization: `Bearer `,
123
+ environment: environment || undefined,
124
+ },
125
+ }
126
+ );
127
+ if (response && response.data && response.data.errorMessage) {
128
+ onError(response.data.errorMessage);
129
+ setData([]);
130
+ onChangeData([]);
131
+ setColumns([]);
132
+ onChangeColumns([]);
133
+ setFields([]);
134
+ return;
135
+ }
136
+ setData(response.data.rows);
137
+ onChangeData(response.data.rows);
138
+ setColumns(response.data.fields.map(elem => convertPostgresColumn(elem)));
139
+ onChangeColumns(
140
+ response.data.fields.map(elem => convertPostgresColumn(elem))
141
+ );
142
+ setFields(response.data.fields);
143
+ };
144
+
145
+ if (!schema.length) {
146
+ return null;
147
+ }
148
+
149
+ return (
150
+ <ReportingTool
151
+ theme={theme}
152
+ data={data}
153
+ schema={schema}
154
+ onChangeQuery={onChangeQuery}
155
+ runQuery={runQuery}
156
+ SelectComponent={SelectComponent}
157
+ />
158
+ );
159
+ }
160
+
161
+ export function getPostgresBasicType(column) {
162
+ let format;
163
+
164
+ // first check if column.dataTypeID exists
165
+ if (column.dataTypeID) {
166
+ switch (column.dataTypeID) {
167
+ case 20: // int8
168
+ case 21: // int2
169
+ case 23: // int4
170
+ case 700: // float4
171
+ case 701: // float8
172
+ case 1700: // numeric
173
+ format = 'number';
174
+ break;
175
+ case 1082: // date
176
+ case 1083: // time
177
+ case 1184: // timestamptz
178
+ case 1114: // timestamp
179
+ format = 'date';
180
+ break;
181
+ case 1043: // varchar
182
+ default:
183
+ format = 'string';
184
+ }
185
+ } else if (column.fieldType) {
186
+ // if column.dataTypeID doesn't exist, check column.fieldType
187
+ switch (column.fieldType) {
188
+ case 'int8':
189
+ case 'int2':
190
+ case 'int4':
191
+ case 'float4':
192
+ case 'float8':
193
+ case 'numeric':
194
+ format = 'number';
195
+ break;
196
+ case 'date':
197
+ case 'time':
198
+ case 'timestamptz':
199
+ case 'timestamp':
200
+ format = 'date';
201
+ break;
202
+ case 'varchar':
203
+ default:
204
+ format = 'string';
205
+ }
206
+ }
207
+
208
+ return format;
209
+ }
210
+
211
+ function ReportingTool({
212
+ schema,
213
+ data,
214
+ runQuery,
215
+ SelectComponent,
216
+ onChangeQuery,
217
+ theme,
218
+ }) {
219
+ const [selectedTable, setSelectedTable] = useState(schema[0]);
220
+ const [selectedColumn, setSelectedColumn] = useState(schema[0].columns[0]);
221
+ const [filters, setFilters] = useState([]);
222
+ const [AST, setAST] = useState({
223
+ with: null,
224
+ type: 'select',
225
+ options: null,
226
+ distinct: { type: null },
227
+ columns: '*',
228
+ into: { position: null },
229
+ from: [{ db: null, table: schema[0].displayName, as: null }],
230
+ where: null,
231
+ groupby: null,
232
+ having: null,
233
+ orderby: null,
234
+ limit: { seperator: '', value: [] },
235
+ window: null,
236
+ });
237
+ const [numberStart, setNumberStart] = useState(0);
238
+ const [numberEnd, setNumberEnd] = useState(0);
239
+ const [dateStart, setDateStart] = useState('');
240
+ const [dateEnd, setDateEnd] = useState('');
241
+ const [computedColumns, setComputedColumns] = useState({});
242
+ const [stringFilterValues, setStringFilterValues] = useState([]);
243
+ const [columnType, setColumnType] = useState(
244
+ getPostgresBasicType(schema[0].columns[0])
245
+ );
246
+ const [sqlQuery, setSqlQuery] = useState('');
247
+ // month | week | day | quarter
248
+ const [dateBucket, setDateBucket] = useState('month');
249
+ const [indexBeingEdited, setIndexBeingEdited] = useState(-1);
250
+ const [groupBys, setGroupBys] = useState([]);
251
+ const [aggregations, setAggregations] = useState([]);
252
+ const [selectedGroupByColumn, setSelectedGroupByColumn] = useState(
253
+ schema[0].columns[0]
254
+ );
255
+ const [groupByColumnType, setGroupByColumnType] = useState(
256
+ getPostgresBasicType(schema[0].columns[0])
257
+ );
258
+ const [selectedSortByColumn, setSelectedSortByColumn] = useState(
259
+ schema[0].columns[0].name
260
+ );
261
+ const [sortBys, setSortBys] = useState([]);
262
+ const [selectedSortByDirection, setSelectedSortByDirection] =
263
+ useState('ascending');
264
+
265
+ const [groupByIndexBeingEdited, setGroupByIndexBeingEdited] = useState(-1);
266
+
267
+ const [sortByIndexBeingEdited, setSortByIndexBeingEdited] = useState(-1);
268
+ const [sortableColumns, setSortableColumns] = useState(
269
+ schema[0].columns.map(col => col.name)
270
+ );
271
+
272
+ const addGroupBy = () => {
273
+ if (selectedGroupByColumn && groupByColumnType) {
274
+ if (groupByColumnType === 'string') {
275
+ setGroupBys(groupBys => {
276
+ return [
277
+ ...groupBys,
278
+ {
279
+ column: selectedGroupByColumn.name,
280
+ columnType: groupByColumnType,
281
+ tag: selectedGroupByColumn.name,
282
+ },
283
+ ];
284
+ });
285
+ return;
286
+ } else if (groupByColumnType === 'number') {
287
+ // is this possible lmao
288
+ return;
289
+ } else if (groupByColumnType === 'date') {
290
+ setGroupBys(groupBys => {
291
+ return [
292
+ ...groupBys,
293
+ {
294
+ column: selectedGroupByColumn.name,
295
+ columnType: groupByColumnType,
296
+ bucket: dateBucket,
297
+ tag: dateBucket,
298
+ },
299
+ ];
300
+ });
301
+ return;
302
+ }
303
+ }
304
+ };
305
+
306
+ const addSortBy = () => {
307
+ setSortBys(sortBys => {
308
+ return [
309
+ ...sortBys,
310
+ {
311
+ column: selectedSortByColumn,
312
+ direction: selectedSortByDirection,
313
+ },
314
+ ];
315
+ });
316
+ };
317
+
318
+ const removeSortBy = index => {
319
+ setSortBys(oldSortBys => {
320
+ const newSortBys = [...oldSortBys];
321
+ newSortBys.splice(index, 1);
322
+ return newSortBys;
323
+ });
324
+ setSortByIndexBeingEdited(-1);
325
+ };
326
+
327
+ const removeGroupBy = index => {
328
+ const columnBeingDeleted = groupBys[index].tag;
329
+ setSortBys(oldSortBys => {
330
+ const newSortBys = [
331
+ ...oldSortBys.filter(sortBy => sortBy.column !== columnBeingDeleted),
332
+ ];
333
+ return newSortBys;
334
+ });
335
+ setGroupBys(oldGroupBys => {
336
+ const newGroupBys = [...oldGroupBys];
337
+ newGroupBys.splice(index, 1);
338
+ return newGroupBys;
339
+ });
340
+ setGroupByIndexBeingEdited(-1);
341
+ };
342
+
343
+ const updateSortBy = index => {
344
+ if (selectedSortByColumn && selectedSortByDirection) {
345
+ setSortBys(sortBys => {
346
+ const newSortBys = [...sortBys];
347
+ newSortBys[index] = {
348
+ column: selectedSortByColumn,
349
+ direction: selectedSortByDirection,
350
+ };
351
+ return newSortBys;
352
+ });
353
+ setSortByIndexBeingEdited(-1);
354
+ return;
355
+ }
356
+ };
357
+
358
+ useEffect(() => {
359
+ setColumnType(getPostgresBasicType(selectedColumn));
360
+ // console.log("WTF: ", selectedColumn);
361
+ }, [selectedColumn]);
362
+
363
+ useEffect(() => {
364
+ setGroupByColumnType(getPostgresBasicType(selectedGroupByColumn));
365
+ }, [selectedGroupByColumn]);
366
+
367
+ const selectFilter = index => {
368
+ const filter = filters[index];
369
+ const matchingColumn = selectedTable.columns.find(
370
+ column => column.name === filter.column
371
+ );
372
+ setSelectedColumn(matchingColumn);
373
+ if (filter.columnType === 'string') {
374
+ setStringFilterValues(filter.stringFilterValues);
375
+ setIndexBeingEdited(index);
376
+ } else if (filter.columnType === 'number') {
377
+ setNumberStart(filter.numberStart);
378
+ setNumberEnd(filter.numberEnd);
379
+ setIndexBeingEdited(index);
380
+ } else if (filter.columnType === 'date') {
381
+ setDateStart(filter.dateStart);
382
+ setDateEnd(filter.dateEnd);
383
+ setIndexBeingEdited(index);
384
+ }
385
+ };
386
+
387
+ const selectSortBy = index => {
388
+ const sortBy = sortBys[index];
389
+ setSelectedSortByColumn(sortBy.column);
390
+ setSelectedSortByDirection(sortBy.direction);
391
+ setSortByIndexBeingEdited(index);
392
+ };
393
+
394
+ const selectGroupBy = index => {
395
+ const groupBy = groupBys[index];
396
+ const matchingColumn = selectedTable.columns.find(
397
+ column => column.name === groupBy.column
398
+ );
399
+ setSelectedGroupByColumn(matchingColumn);
400
+ if (groupBy.bucket) {
401
+ setDateBucket(groupBy.bucket);
402
+ }
403
+ setGroupByIndexBeingEdited(index);
404
+ };
405
+
406
+ const updateGroupBy = index => {
407
+ if (selectedGroupByColumn && groupByColumnType) {
408
+ // if column changed, then auto delete the sort using that column
409
+ const columnBeingDeleted =
410
+ selectedGroupByColumn.name !== groupBys[index].column
411
+ ? groupBys[index].tag
412
+ : 'nocolumnbeingdeleted';
413
+ setSortBys(oldSortBys => {
414
+ const newSortBys = [
415
+ ...oldSortBys.filter(sortBy => sortBy.column !== columnBeingDeleted),
416
+ ];
417
+ return newSortBys;
418
+ });
419
+ if (groupByColumnType === 'date') {
420
+ setGroupBys(groupBys => {
421
+ const newGroupBys = [...groupBys];
422
+ newGroupBys[index] = {
423
+ column: selectedGroupByColumn.name,
424
+ columnType: groupByColumnType,
425
+ bucket: dateBucket,
426
+ tag: dateBucket,
427
+ };
428
+ return newGroupBys;
429
+ });
430
+ } else {
431
+ setGroupBys(groupBys => {
432
+ const newGroupBys = [...groupBys];
433
+ newGroupBys[index] = {
434
+ column: selectedGroupByColumn.name,
435
+ columnType: groupByColumnType,
436
+ tag: selectedGroupByColumn.name,
437
+ };
438
+ return newGroupBys;
439
+ });
440
+ }
441
+ setGroupByIndexBeingEdited(-1);
442
+ return;
443
+ }
444
+ };
445
+
446
+ const updateFilter = index => {
447
+ if (selectedColumn && columnType) {
448
+ if (columnType === 'string') {
449
+ setFilters(filters => {
450
+ const newFilters = [...filters];
451
+ newFilters[index] = {
452
+ column: selectedColumn.name,
453
+ columnType,
454
+ stringFilterValues,
455
+ tag: `${selectedColumn.name} (${stringFilterValues.join(', ')})`,
456
+ };
457
+ return newFilters;
458
+ });
459
+ } else if (columnType === 'number') {
460
+ setFilters(filters => {
461
+ const newFilters = [...filters];
462
+ newFilters[index] = {
463
+ column: selectedColumn.name,
464
+ columnType,
465
+ numberStart,
466
+ numberEnd,
467
+ tag: `${numberStart} < ${selectedColumn.name} < ${numberEnd}`,
468
+ };
469
+ return newFilters;
470
+ });
471
+ } else if (columnType === 'date') {
472
+ setFilters(filters => {
473
+ const newFilters = [...filters];
474
+ newFilters[index] = {
475
+ column: selectedColumn.name,
476
+ columnType,
477
+ dateStart,
478
+ dateEnd,
479
+ tag: `${selectedColumn.name} (${format(
480
+ new Date(dateStart),
481
+ 'MMM dd'
482
+ )} - ${format(new Date(dateEnd), 'MMM dd')})`,
483
+ };
484
+ return newFilters;
485
+ });
486
+ }
487
+
488
+ setStringFilterValues([]);
489
+ setNumberStart(0);
490
+ setNumberEnd(0);
491
+ setDateStart('');
492
+ setDateEnd('');
493
+ setIndexBeingEdited(-1);
494
+ return;
495
+ }
496
+ };
497
+
498
+ // ADD FILTER TO "FILTERS" ARRAY
499
+ const addFilter = () => {
500
+ if (selectedColumn && columnType) {
501
+ // const type = getPostgresBasicType(selectedColumn);
502
+ let newCondition;
503
+
504
+ if (columnType === 'string') {
505
+ setFilters(filters => {
506
+ return [
507
+ ...filters,
508
+ {
509
+ column: selectedColumn.name,
510
+ columnType,
511
+ stringFilterValues,
512
+ tag: `${selectedColumn.name} (${stringFilterValues.join(', ')})`,
513
+ },
514
+ ];
515
+ });
516
+ setStringFilterValues([]);
517
+ setNumberStart(0);
518
+ setNumberEnd(0);
519
+ setDateStart('');
520
+ setDateEnd('');
521
+ return;
522
+ } else if (columnType === 'number') {
523
+ setFilters(filters => {
524
+ return [
525
+ ...filters,
526
+ {
527
+ column: selectedColumn.name,
528
+ columnType,
529
+ numberStart,
530
+ numberEnd,
531
+ tag: `${numberStart} < ${selectedColumn.name} < ${numberEnd}`,
532
+ },
533
+ ];
534
+ });
535
+ setStringFilterValues([]);
536
+ setNumberStart(0);
537
+ setNumberEnd(0);
538
+ setDateStart('');
539
+ setDateEnd('');
540
+ return;
541
+ } else if (columnType === 'date') {
542
+ setFilters(filters => {
543
+ return [
544
+ ...filters,
545
+ {
546
+ column: selectedColumn.name,
547
+ columnType,
548
+ dateStart,
549
+ dateEnd,
550
+ tag: `${selectedColumn.name} (${format(
551
+ new Date(dateStart),
552
+ 'MMM dd'
553
+ )} - ${format(new Date(dateEnd), 'MMM dd')})`,
554
+ },
555
+ ];
556
+ });
557
+ setStringFilterValues([]);
558
+ setNumberStart(0);
559
+ setNumberEnd(0);
560
+ setDateStart('');
561
+ setDateEnd('');
562
+ }
563
+ }
564
+ };
565
+
566
+ const setAggregationColumn = (column, index) => {
567
+ // ex
568
+ setAggregations(aggregations => {
569
+ const newAggregations = [...aggregations];
570
+ newAggregations[index] = {
571
+ ...newAggregations[index],
572
+ column: column,
573
+ };
574
+ return newAggregations;
575
+ });
576
+ };
577
+
578
+ const setAggregationType = (aggregationType, index) => {
579
+ // ex
580
+ setAggregations(aggregations => {
581
+ const newAggregations = [...aggregations];
582
+ newAggregations[index] = {
583
+ ...newAggregations[index],
584
+ aggregationType: aggregationType,
585
+ };
586
+ return newAggregations;
587
+ });
588
+ };
589
+
590
+ const addAggregation = () => {
591
+ // setAggregations([
592
+ // {
593
+ // column: selectedTable.columns.filter(
594
+ // (col) => getPostgresBasicType(col) === "number"
595
+ // )[0],
596
+ // aggregationType: "sum",
597
+ // },
598
+ // ]);
599
+ setAggregations(aggregations => {
600
+ const newAggregations = [
601
+ ...aggregations,
602
+ {
603
+ column: selectedTable.columns.filter(
604
+ col =>
605
+ getPostgresBasicType(col) === 'number' &&
606
+ !aggregations.map(elem => elem.name).includes(col.name)
607
+ )[0],
608
+ aggregationType: 'sum',
609
+ },
610
+ ];
611
+ return newAggregations;
612
+ });
613
+ };
614
+
615
+ useEffect(() => {
616
+ // if selected table changes, clear everything
617
+ if (selectedTable.displayName !== AST.from.table) {
618
+ setSelectedColumn(selectedTable.columns[0]);
619
+ setSortableColumns(selectedTable.columns.map(col => col.name));
620
+ setGroupBys([]);
621
+ setSortBys([]);
622
+ setFilters([]);
623
+ // setAST((AST) => {
624
+ // return {
625
+ // with: null,
626
+ // type: "select",
627
+ // options: null,
628
+ // distinct: { type: null },
629
+ // columns: "*",
630
+ // into: { position: null },
631
+ // where: null,
632
+ // groupby: null,
633
+ // having: null,
634
+ // orderby: null,
635
+ // limit: { seperator: "", value: [] },
636
+ // window: null,
637
+ // from: [{ db: null, table: selectedTable.displayName, as: null }],
638
+ // };
639
+ // });
640
+ return;
641
+ }
642
+ }, [selectedTable]);
643
+
644
+ // USE EFFECT HOOK THAT TRANSFORMS "FILTERS ARRAY INTO AST"
645
+ useEffect(() => {
646
+ if (filters.length || groupBys.length || aggregations.length) {
647
+ let newAST = {
648
+ with: null,
649
+ type: 'select',
650
+ options: null,
651
+ distinct: null,
652
+ columns: null,
653
+ into: { position: null },
654
+ from: [{ db: null, table: selectedTable.displayName, as: null }],
655
+ where: null,
656
+ groupby: null,
657
+ having: null,
658
+ orderby: null,
659
+ limit: null,
660
+ window: null,
661
+ };
662
+ // FILTERS
663
+ for (let i = 0; i < filters.length; i++) {
664
+ const filter = filters[i];
665
+ const {
666
+ column,
667
+ columnType,
668
+ stringFilterValues,
669
+ numberStart,
670
+ numberEnd,
671
+ dateStart,
672
+ dateEnd,
673
+ } = filter;
674
+ let newCondition;
675
+ if (columnType === 'string') {
676
+ newCondition = {
677
+ type: 'binary_expr',
678
+ operator: 'IN',
679
+ left: {
680
+ type: 'column_ref',
681
+ table: null,
682
+ column: column,
683
+ },
684
+ right: {
685
+ type: 'expr_list',
686
+ value: stringFilterValues.map(value => ({
687
+ type: 'single_quote_string',
688
+ value,
689
+ })),
690
+ },
691
+ };
692
+ } else if (columnType === 'number') {
693
+ newCondition = {
694
+ type: 'binary_expr',
695
+ operator: 'BETWEEN',
696
+ left: {
697
+ type: 'column_ref',
698
+ table: null,
699
+ column: column,
700
+ },
701
+ right: {
702
+ type: 'expr_list',
703
+ value: [
704
+ { type: 'number', value: numberStart },
705
+ { type: 'number', value: numberEnd },
706
+ ],
707
+ },
708
+ };
709
+ } else if (columnType === 'date') {
710
+ newCondition = {
711
+ type: 'binary_expr',
712
+ operator: 'BETWEEN',
713
+ left: {
714
+ type: 'column_ref',
715
+ table: null,
716
+ column: column,
717
+ },
718
+ right: {
719
+ type: 'expr_list',
720
+ value: [
721
+ {
722
+ type: 'single_quote_string',
723
+ value: format(new Date(dateStart), 'MM/dd/yyyy'),
724
+ },
725
+ {
726
+ type: 'single_quote_string',
727
+ value: format(new Date(dateEnd), 'MM/dd/yyyy'),
728
+ },
729
+ ],
730
+ },
731
+ };
732
+ }
733
+
734
+ if (!newAST.where) {
735
+ newAST.where = newCondition;
736
+ } else {
737
+ newAST.where = {
738
+ type: 'binary_expr',
739
+ operator: 'AND',
740
+ left: newAST.where,
741
+ right: newCondition,
742
+ };
743
+ }
744
+ }
745
+
746
+ // GROUP BYS and AGGREGATIONS
747
+ // if (groupBys.length > 0) {
748
+ // newAST.groupby = groupBys.map((groupBy) => ({
749
+ // type: "column_ref",
750
+ // table: null,
751
+ // column: groupBy.column,
752
+ // }));
753
+
754
+ // if (aggregations.length > 0) {
755
+ // newAST.columns = [
756
+ // ...groupBys.map((groupBy) => ({
757
+ // expr: {
758
+ // type: "column_ref",
759
+ // table: null,
760
+ // column: groupBy.column,
761
+ // },
762
+ // as: null,
763
+ // })),
764
+ // ...aggregations.map((aggregation) => ({
765
+ // expr: {
766
+ // type: "aggr_func",
767
+ // name: aggregation.aggregationType.toUpperCase(),
768
+ // args: {
769
+ // expr: {
770
+ // type: "column_ref",
771
+ // table: null,
772
+ // column: aggregation.column.name,
773
+ // },
774
+ // },
775
+ // over: null,
776
+ // },
777
+ // as: null,
778
+ // })),
779
+ // ];
780
+ // } else {
781
+ // newAST.columns = groupBys.map((groupBy) => ({
782
+ // expr: {
783
+ // type: "column_ref",
784
+ // table: null,
785
+ // column: groupBy.column,
786
+ // },
787
+ // as: null,
788
+ // }));
789
+ // }
790
+ // } else {
791
+ // newAST.columns = "*";
792
+ // newAST.groupby = null;
793
+ // }
794
+
795
+ // GROUP BYS
796
+ if (groupBys.length > 0) {
797
+ newAST.columns = [];
798
+ newAST.groupby = [];
799
+
800
+ for (let i = 0; i < groupBys.length; i++) {
801
+ const groupBy = groupBys[i];
802
+ if (groupBy.columnType === 'date') {
803
+ newAST.columns.push({
804
+ expr: {
805
+ type: 'function',
806
+ name: 'date_trunc',
807
+ args: {
808
+ type: 'expr_list',
809
+ value: [
810
+ {
811
+ type: 'single_quote_string',
812
+ value: groupBy.bucket,
813
+ },
814
+ {
815
+ type: 'column_ref',
816
+ table: null,
817
+ column: groupBy.column,
818
+ },
819
+ ],
820
+ },
821
+ over: null,
822
+ },
823
+ as: groupBy.bucket,
824
+ });
825
+ newAST.groupby.push({
826
+ type: 'column_ref',
827
+ table: null,
828
+ column: groupBy.bucket,
829
+ });
830
+ } else {
831
+ newAST.columns.push({
832
+ expr: {
833
+ type: 'column_ref',
834
+ table: null,
835
+ column: groupBy.column,
836
+ },
837
+ as: null,
838
+ });
839
+ newAST.groupby.push({
840
+ type: 'column_ref',
841
+ table: null,
842
+ column: groupBy.column,
843
+ });
844
+ }
845
+ }
846
+
847
+ // AGGREGATIONS
848
+ if (aggregations.length > 0) {
849
+ for (let i = 0; i < aggregations.length; i++) {
850
+ const aggregation = aggregations[i];
851
+ newAST.columns.push({
852
+ expr: {
853
+ type: 'aggr_func',
854
+ name: aggregation.aggregationType.toUpperCase(),
855
+ args: {
856
+ expr: {
857
+ type: 'column_ref',
858
+ table: null,
859
+ column: aggregation.column.name,
860
+ },
861
+ },
862
+ over: null,
863
+ },
864
+ as: null,
865
+ });
866
+ }
867
+ }
868
+ } else {
869
+ newAST.columns = '*';
870
+ newAST.groupby = null;
871
+ }
872
+
873
+ if (sortBys.length > 0) {
874
+ newAST.orderby = [];
875
+ for (let i = 0; i < sortBys.length; i++) {
876
+ const sortBy = sortBys[i];
877
+ newAST.orderby.push({
878
+ expr: { type: 'column_ref', table: null, column: sortBy.column },
879
+ type: sortBy.direction === 'descending' ? 'DESC' : 'ASC',
880
+ });
881
+ }
882
+ // "orderby":[{"expr":{"type":"column_ref","table":null,"column":"amount"},"type":"DESC"},{"expr":{"type":"column_ref","table":null,"column":"month"},"type":"ASC"}]
883
+ }
884
+
885
+ setAST(newAST);
886
+ }
887
+ }, [filters, groupBys, aggregations, sortBys]);
888
+
889
+ const removeFilter = index => {
890
+ setFilters(oldFilters => {
891
+ const newFilters = [...oldFilters];
892
+ newFilters.splice(index, 1);
893
+ return newFilters;
894
+ });
895
+ setIndexBeingEdited(-1);
896
+ };
897
+
898
+ const computeStats = useCallback(
899
+ column => {
900
+ if (!computedColumns[column.name] && data) {
901
+ const basicType = getPostgresBasicType(column);
902
+ let result;
903
+
904
+ if (basicType === 'number') {
905
+ let min = Infinity,
906
+ max = -Infinity;
907
+ data.forEach(row => {
908
+ const value = row[column.name];
909
+ min = Math.min(min, value);
910
+ max = Math.max(max, value);
911
+ });
912
+ result = { min, max };
913
+ } else if (basicType === 'string') {
914
+ const freqMap = {};
915
+ data.forEach(row => {
916
+ const value = row[column.name];
917
+ freqMap[value] = (freqMap[value] || 0) + 1;
918
+ });
919
+ result = Object.entries(freqMap)
920
+ .sort((a, b) => b[1] - a[1])
921
+ .slice(0, 6)
922
+ .map(([key]) => key);
923
+ } else {
924
+ // Handle other column types if necessary
925
+ }
926
+
927
+ setComputedColumns({
928
+ ...computedColumns,
929
+ [column.name]: result,
930
+ });
931
+ }
932
+ },
933
+ [data, computedColumns]
934
+ );
935
+
936
+ // Call this function whenever the selected column changes
937
+ useEffect(() => {
938
+ computeStats(selectedColumn);
939
+ }, [selectedColumn]);
940
+
941
+ // Use the results directly in your component
942
+ const columnStats = computedColumns[selectedColumn.name];
943
+
944
+ // useEffect(() => {
945
+ // if (AST && AST.from[0].table) {
946
+ // const parser = new Parser();
947
+ // const sqlQuery = parser.sqlify(AST, { database: "PostgresQL" });
948
+ // if (sqlQuery) {
949
+ // runQuery(sqlQuery);
950
+ // return;
951
+ // }
952
+ // }
953
+ // }, [AST]);
954
+
955
+ useEffect(() => {
956
+ if (!aggregations.length) {
957
+ setAggregations([
958
+ {
959
+ column: selectedTable.columns.filter(
960
+ col => getPostgresBasicType(col) === 'number'
961
+ )[0],
962
+ aggregationType: 'sum',
963
+ },
964
+ ]);
965
+ }
966
+ }, [selectedGroupByColumn]);
967
+
968
+ useEffect(() => {
969
+ const getSqlQueryFromAST = async () => {
970
+ try {
971
+ if (AST && AST.from[0].table) {
972
+ const response = await axios.post(
973
+ 'https://quill-344421.uc.r.appspot.com/sqlify',
974
+ { ast: AST }
975
+ );
976
+ const sqlQuery = response.data.query; // assuming the response contains the SQL query
977
+ // alert(sqlQuery);
978
+ if (sqlQuery) {
979
+ onChangeQuery(sqlQuery);
980
+ runQuery(sqlQuery);
981
+ setSqlQuery(sqlQuery);
982
+ if (AST.columns === '*') {
983
+ setSortableColumns(selectedTable.columns.map(col => col.name));
984
+ } else if (AST.columns.length) {
985
+ setSortableColumns(
986
+ AST.columns.map(elem => elem.as || elem.expr.name)
987
+ );
988
+ }
989
+ }
990
+ }
991
+ } catch (err) {
992
+ console.error(err);
993
+ }
994
+ };
995
+
996
+ getSqlQueryFromAST();
997
+ }, [AST]);
998
+
999
+ useEffect(() => {
1000
+ if (sortableColumns.length) {
1001
+ setSelectedSortByColumn(sortableColumns[0]);
1002
+ }
1003
+ }, [sortableColumns]);
1004
+
1005
+ if (!schema || !schema.length) {
1006
+ return null;
1007
+ }
1008
+
1009
+ return (
1010
+ <div style={{ marginLeft: '25px' }}>
1011
+ <div
1012
+ style={{
1013
+ fontSize: '0.8rem',
1014
+ marginBottom: '6px',
1015
+ fontWeight: 'bold',
1016
+ color: theme.secondaryTextColor,
1017
+ }}
1018
+ >
1019
+ Table
1020
+ </div>
1021
+ {/* <br />
1022
+ <div>what ast should be</div>
1023
+ <div>{JSON.stringify(lmao)}</div>
1024
+ <br /> */}
1025
+ {/* <div>actual ast</div>
1026
+ <div>{JSON.stringify(AST)}</div>
1027
+ <br /> */}
1028
+ {/* <div>actual sql query</div>
1029
+ <div>{sqlQuery}</div>
1030
+ <br /> */}
1031
+ {/* <div>group bys</div>
1032
+ <div>{JSON.stringify(groupBys)}</div>
1033
+ <div>aggregations</div>
1034
+ <div>{JSON.stringify(aggregations)}</div> */}
1035
+
1036
+ {/* <Dropdown
1037
+ value={selectedTable.name}
1038
+ onChange={(e) => {
1039
+ const table = schema.find((t) => t.name === e);
1040
+ setSelectedTable(table);
1041
+ }}
1042
+ options={schema.map((elem) => {
1043
+ return { label: elem.name, value: elem.name };
1044
+ })}
1045
+ /> */}
1046
+ <SelectComponent
1047
+ value={selectedTable.displayName}
1048
+ onChange={e => {
1049
+ const table = schema.find(t => t.displayName === e);
1050
+ setSelectedTable(table);
1051
+ }}
1052
+ options={
1053
+ schema?.length
1054
+ ? schema.map(elem => {
1055
+ return { label: elem.displayName, value: elem.displayName };
1056
+ })
1057
+ : []
1058
+ }
1059
+ />
1060
+ {/* <SelectComponent
1061
+ value={selectedTable.name}
1062
+ onChange={(e) => {
1063
+ const table = schema.find((t) => t.name === e);
1064
+ setSelectedTable(table);
1065
+ }}
1066
+ options={schema.map((elem) => {
1067
+ return { label: elem.name, value: elem.name };
1068
+ })}
1069
+ /> */}
1070
+ <div
1071
+ style={{ display: 'flex', flexDirection: 'column', marginTop: '12px' }}
1072
+ >
1073
+ {/* <AddFilterModal
1074
+ filters={filters}
1075
+ selectedColumn={selectedColumn}
1076
+ numberStart={numberStart}
1077
+ numberEnd={numberEnd}
1078
+ dateStart={dateStart}
1079
+ setDateStart={setDateStart}
1080
+ columnStats={columnStats}
1081
+ stringFilterValues={stringFilterValues}
1082
+ setStringFilterValues={setStringFilterValues}
1083
+ addFilter={addFilter}
1084
+ setSelectedColumn={setSelectedColumn}
1085
+ setNumberStart={setNumberStart}
1086
+ setNumberEnd={setNumberEnd}
1087
+ selectedTable={selectedTable}
1088
+ columnType={columnType}
1089
+ dateEnd={dateEnd}
1090
+ setDateEnd={setDateEnd}
1091
+ removeFilter={removeFilter}
1092
+ selectFilter={selectFilter}
1093
+ ref={ref}
1094
+ indexBeingEdited={indexBeingEdited}
1095
+ updateFilter={updateFilter}
1096
+ SelectComponent={SelectComponent}
1097
+ /> */}
1098
+
1099
+ <AddFilterModal2
1100
+ filters={filters}
1101
+ selectedColumn={selectedColumn}
1102
+ numberStart={numberStart}
1103
+ numberEnd={numberEnd}
1104
+ dateStart={dateStart}
1105
+ setDateStart={setDateStart}
1106
+ columnStats={columnStats}
1107
+ stringFilterValues={stringFilterValues}
1108
+ setStringFilterValues={setStringFilterValues}
1109
+ addFilter={addFilter}
1110
+ setSelectedColumn={setSelectedColumn}
1111
+ setNumberStart={setNumberStart}
1112
+ setNumberEnd={setNumberEnd}
1113
+ selectedTable={selectedTable}
1114
+ columnType={columnType}
1115
+ dateEnd={dateEnd}
1116
+ setDateEnd={setDateEnd}
1117
+ removeFilter={removeFilter}
1118
+ selectFilter={selectFilter}
1119
+ indexBeingEdited={indexBeingEdited}
1120
+ updateFilter={updateFilter}
1121
+ SelectComponent={SelectComponent}
1122
+ theme={theme}
1123
+ />
1124
+
1125
+ {/* <div style={{ height: 12 }} /> */}
1126
+
1127
+ <GroupByModal2
1128
+ selectedTable={selectedTable}
1129
+ groupBys={groupBys}
1130
+ selectedGroupByColumn={selectedGroupByColumn}
1131
+ setSelectedGroupByColumn={setSelectedGroupByColumn}
1132
+ addGroupBy={addGroupBy}
1133
+ groupByColumnType={groupByColumnType}
1134
+ removeGroupBy={removeGroupBy}
1135
+ selectGroupBy={selectGroupBy}
1136
+ groupByIndexBeingEdited={groupByIndexBeingEdited}
1137
+ updateGroupBy={updateGroupBy}
1138
+ aggregations={aggregations}
1139
+ setAggregationColumn={setAggregationColumn}
1140
+ setAggregationType={setAggregationType}
1141
+ SelectComponent={SelectComponent}
1142
+ addAggregation={addAggregation}
1143
+ dateBucket={dateBucket}
1144
+ setDateBucket={setDateBucket}
1145
+ theme={theme}
1146
+ />
1147
+
1148
+ <SortByModal
1149
+ selectedTable={selectedTable}
1150
+ sortableColumns={sortableColumns}
1151
+ sortBys={sortBys}
1152
+ selectedSortByColumn={selectedSortByColumn}
1153
+ setSelectedSortByColumn={setSelectedSortByColumn}
1154
+ selectedSortByDirection={selectedSortByDirection}
1155
+ setSelectedSortByDirection={setSelectedSortByDirection}
1156
+ addSortBy={addSortBy}
1157
+ removeSortBy={removeSortBy}
1158
+ selectSortBy={selectSortBy}
1159
+ sortByIndexBeingEdited={sortByIndexBeingEdited}
1160
+ updateSortBy={updateSortBy}
1161
+ SelectComponent={SelectComponent}
1162
+ theme={theme}
1163
+ />
1164
+ </div>
1165
+ </div>
1166
+ );
1167
+ }
1168
+
1169
+ function FilterTag({ id, label, removeFilter, index, selectFilter, theme }) {
1170
+ const handleRemoveFilter = () => {
1171
+ removeFilter(index);
1172
+ };
1173
+ const handleSelectFilter = () => {
1174
+ selectFilter(index);
1175
+ };
1176
+ return (
1177
+ <div
1178
+ id={id}
1179
+ onClick={handleSelectFilter}
1180
+ style={{
1181
+ marginLeft: '12px',
1182
+ cursor: 'pointer',
1183
+ border: '1px',
1184
+ borderColor: '#EFF0FC',
1185
+ borderRadius: 6,
1186
+ backgroundColor: '#EFF0FC',
1187
+ paddingLeft: '1rem',
1188
+ paddingRight: '0.5rem',
1189
+ // paddingTop: "0.44rem",
1190
+ // paddingBottom: "0.44rem",
1191
+ height: 35,
1192
+ display: 'flex',
1193
+ alignItems: 'center',
1194
+ fontSize: 14,
1195
+ fontWeight: 'medium',
1196
+ color: theme.primaryTextColor,
1197
+ outline: 'none',
1198
+ // ring: "2",
1199
+ // ringColor: "white",
1200
+ // ringOpacity: "75",
1201
+ }}
1202
+ >
1203
+ <div id={id} style={{ textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
1204
+ {label}
1205
+ </div>
1206
+ <div
1207
+ // onClick={handleRemoveFilter}
1208
+ onClick={e => {
1209
+ e.stopPropagation(); // Prevents the event from bubbling up to the parent
1210
+ handleRemoveFilter();
1211
+ }}
1212
+ style={{
1213
+ display: 'flex',
1214
+ flexDirection: 'row',
1215
+ alignItems: 'center',
1216
+ cursor: 'pointer',
1217
+ paddingLeft: '0.375rem',
1218
+ }}
1219
+ >
1220
+ <XMarkIcon
1221
+ style={{
1222
+ height: '1.25rem',
1223
+ width: '1.25rem',
1224
+ color: theme.primaryTextColor,
1225
+ }}
1226
+ aria-hidden="true"
1227
+ />
1228
+ </div>
1229
+ </div>
1230
+ );
1231
+ }
1232
+
1233
+ const SortByModal = ({
1234
+ selectedSortByColumn,
1235
+ selectedSortByDirection,
1236
+ setSelectedSortByColumn,
1237
+ setSelectedSortByDirection,
1238
+ selectedTable,
1239
+ sortableColumns,
1240
+ removeSortBy,
1241
+ selectSortBy,
1242
+ updateSortBy,
1243
+ addSortBy,
1244
+ sortBys,
1245
+ SelectComponent,
1246
+ sortByIndexBeingEdited,
1247
+ theme,
1248
+ }) => {
1249
+ const dropdownRef = useRef(null);
1250
+ const [isOpen, setIsOpen] = useState(false);
1251
+
1252
+ useEffect(() => {
1253
+ // Event listener to close the dropdown menu when clicking outside
1254
+ const handleOutsideClick = event => {
1255
+ if (
1256
+ event.target.id === 'sort-toggle-button' ||
1257
+ event.target.id === 'sort-tag'
1258
+ ) {
1259
+ setIsOpen(isOpen => !isOpen);
1260
+ return;
1261
+ }
1262
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
1263
+ setIsOpen(false);
1264
+ }
1265
+ };
1266
+
1267
+ // Attach the event listener to the document
1268
+ document.addEventListener('click', handleOutsideClick);
1269
+
1270
+ // Clean up the event listener on component unmount
1271
+ return () => {
1272
+ document.removeEventListener('click', handleOutsideClick);
1273
+ };
1274
+ }, []);
1275
+ return (
1276
+ <div style={{ display: 'flex', flexDirection: 'column', marginTop: 12 }}>
1277
+ <div
1278
+ style={{
1279
+ position: 'relative',
1280
+ display: 'inline-block',
1281
+ textAlign: 'left',
1282
+ }}
1283
+ >
1284
+ <div
1285
+ style={{
1286
+ display: 'flex',
1287
+ flexDirection: 'row',
1288
+ alignItems: 'center',
1289
+ }}
1290
+ >
1291
+ <button
1292
+ id="sort-toggle-button"
1293
+ // onClick={() => setIsOpen((isOpen) => !isOpen)}
1294
+ type="button"
1295
+ aria-haspopup="menu"
1296
+ aria-expanded="true"
1297
+ data-headlessui-state="open"
1298
+ style={{
1299
+ display: 'inline-flex',
1300
+ boxShadow: 'rgba(231, 231, 231, 0.5) 0px 1px 2px 0px',
1301
+ borderRadius: '0.375rem',
1302
+ backgroundColor: 'rgba(0, 0, 0, 0)',
1303
+ padding: '0.375rem 0.75rem',
1304
+ fontSize: '0.875rem',
1305
+ fontWeight: '500',
1306
+ color: theme.primaryTextColor,
1307
+ borderWidth: 1,
1308
+ borderStyle: 'solid',
1309
+ borderColor: theme.borderColor,
1310
+ }}
1311
+ >
1312
+ Sort
1313
+ </button>
1314
+ <div
1315
+ style={{
1316
+ overflowX: 'scroll',
1317
+ display: 'flex',
1318
+ flexDirection: 'row',
1319
+ alignItems: 'center',
1320
+ }}
1321
+ >
1322
+ {sortBys.map((elem, index) => (
1323
+ <FilterTag
1324
+ id="sort-tag"
1325
+ label={elem.column}
1326
+ removeFilter={removeSortBy}
1327
+ selectFilter={selectSortBy}
1328
+ index={index}
1329
+ theme={theme}
1330
+ />
1331
+ ))}
1332
+ </div>
1333
+ </div>
1334
+ {isOpen && (
1335
+ <div
1336
+ ref={dropdownRef}
1337
+ role="menu"
1338
+ tabindex="0"
1339
+ data-headlessui-state="open"
1340
+ style={{
1341
+ zIndex: 120,
1342
+ position: 'absolute',
1343
+ left: '0px',
1344
+ marginTop: '12px',
1345
+ transformOrigin: 'right top',
1346
+ display: 'flex',
1347
+ flexDirection: 'column',
1348
+ padding: '1rem',
1349
+ borderRadius: '0.375rem',
1350
+ backgroundColor: 'rgb(255, 255, 255)',
1351
+ boxShadow: 'rgba(0, 0, 0, 0.2) 0px 1px 5px 0px',
1352
+ }}
1353
+ className="transform opacity-100 scale-100"
1354
+ >
1355
+ <div
1356
+ style={{
1357
+ display: 'flex',
1358
+ flexDirection: 'row',
1359
+ alignItems: 'center',
1360
+ }}
1361
+ >
1362
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
1363
+ <div
1364
+ style={{
1365
+ fontSize: '0.875rem',
1366
+ marginBottom: '6px',
1367
+ fontWeight: '600',
1368
+ color: '#384151',
1369
+ opacity: 0.7,
1370
+ }}
1371
+ >
1372
+ Column
1373
+ </div>
1374
+ {/* select column */}
1375
+ <SelectComponent
1376
+ value={selectedSortByColumn}
1377
+ onChange={e => {
1378
+ setSelectedSortByColumn(e);
1379
+ }}
1380
+ options={sortableColumns.map(elem => {
1381
+ return { label: elem, value: elem };
1382
+ })}
1383
+ />
1384
+ </div>
1385
+ </div>
1386
+ {/* Select bucket (if date) */}
1387
+
1388
+ {/* Select aggregations */}
1389
+ <div
1390
+ style={{
1391
+ fontSize: '0.875rem',
1392
+ marginBottom: '6px',
1393
+ fontWeight: '600',
1394
+ color: '#384151',
1395
+ opacity: 0.7,
1396
+ marginTop: 20,
1397
+ }}
1398
+ >
1399
+ Direction
1400
+ </div>
1401
+
1402
+ <SelectComponent
1403
+ value={selectedSortByDirection}
1404
+ onChange={e => {
1405
+ setSelectedSortByDirection(e);
1406
+ }}
1407
+ options={[
1408
+ { label: 'ascending', value: 'ascending' },
1409
+ { label: 'descending', value: 'descending' },
1410
+ ]}
1411
+ />
1412
+ <button
1413
+ style={{
1414
+ width: '15.3125rem',
1415
+ marginTop: '1.25rem',
1416
+ paddingTop: '0.5rem',
1417
+ paddingBottom: '0.5rem',
1418
+ paddingLeft: '0.75rem',
1419
+ paddingRight: '0.75rem',
1420
+ backgroundColor: '#384151',
1421
+ // hover: { opacity: "90" },
1422
+ color: 'white',
1423
+ fontWeight: '500',
1424
+ borderRadius: '0.375rem',
1425
+ boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
1426
+ }}
1427
+ onClick={() => {
1428
+ if (sortByIndexBeingEdited > -1) {
1429
+ updateSortBy(sortByIndexBeingEdited);
1430
+ setIsOpen(false);
1431
+ // close();
1432
+ return;
1433
+ }
1434
+ addSortBy();
1435
+ setIsOpen(false);
1436
+ // close();
1437
+ }}
1438
+ >
1439
+ {sortByIndexBeingEdited > -1 ? 'Edit sort' : 'Add sort'}
1440
+ </button>
1441
+ </div>
1442
+ )}
1443
+ </div>
1444
+ </div>
1445
+ );
1446
+ };
1447
+
1448
+ const GroupByModal2 = ({
1449
+ selectedGroupByColumn,
1450
+ addGroupBy,
1451
+ setSelectedGroupByColumn,
1452
+ selectedTable,
1453
+ groupByColumnType,
1454
+ groupByIndexBeingEdited,
1455
+ updateGroupBy,
1456
+ groupBys,
1457
+ removeGroupBy,
1458
+ selectGroupBy,
1459
+ SelectComponent,
1460
+ aggregations,
1461
+ setAggregationColumn,
1462
+ setAggregationType,
1463
+ addAggregation,
1464
+ dateBucket,
1465
+ setDateBucket,
1466
+ theme,
1467
+ }) => {
1468
+ const dropdownRef = useRef(null);
1469
+ const [isOpen, setIsOpen] = useState(false);
1470
+
1471
+ useEffect(() => {
1472
+ // Event listener to close the dropdown menu when clicking outside
1473
+ const handleOutsideClick = event => {
1474
+ if (
1475
+ event.target.id === 'group-toggle-button' ||
1476
+ event.target.id === 'group-tag'
1477
+ ) {
1478
+ setIsOpen(isOpen => !isOpen);
1479
+ return;
1480
+ }
1481
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
1482
+ setIsOpen(false);
1483
+ }
1484
+ };
1485
+
1486
+ // Attach the event listener to the document
1487
+ document.addEventListener('click', handleOutsideClick);
1488
+
1489
+ // Clean up the event listener on component unmount
1490
+ return () => {
1491
+ document.removeEventListener('click', handleOutsideClick);
1492
+ };
1493
+ }, []);
1494
+ return (
1495
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
1496
+ <div
1497
+ style={{
1498
+ position: 'relative',
1499
+ display: 'inline-block',
1500
+ textAlign: 'left',
1501
+ }}
1502
+ >
1503
+ <div
1504
+ style={{
1505
+ display: 'flex',
1506
+ flexDirection: 'row',
1507
+ alignItems: 'center',
1508
+ }}
1509
+ >
1510
+ <button
1511
+ id="group-toggle-button"
1512
+ // onClick={() => setIsOpen((isOpen) => !isOpen)}
1513
+ type="button"
1514
+ aria-haspopup="menu"
1515
+ aria-expanded="true"
1516
+ data-headlessui-state="open"
1517
+ style={{
1518
+ display: 'inline-flex',
1519
+ boxShadow: 'rgba(231, 231, 231, 0.5) 0px 1px 2px 0px',
1520
+ borderRadius: '0.375rem',
1521
+ backgroundColor: 'rgba(0, 0, 0, 0)',
1522
+ padding: '0.375rem 0.75rem',
1523
+ fontSize: '0.875rem',
1524
+ fontWeight: '500',
1525
+ color: theme.primaryTextColor,
1526
+ borderWidth: 1,
1527
+ borderStyle: 'solid',
1528
+ borderColor: theme.borderColor,
1529
+ }}
1530
+ >
1531
+ Groups
1532
+ </button>
1533
+ <div
1534
+ style={{
1535
+ overflowX: 'scroll',
1536
+ display: 'flex',
1537
+ flexDirection: 'row',
1538
+ alignItems: 'center',
1539
+ }}
1540
+ >
1541
+ {groupBys.map((elem, index) => (
1542
+ <FilterTag
1543
+ id="group-tag"
1544
+ label={elem.tag}
1545
+ removeFilter={removeGroupBy}
1546
+ selectFilter={selectGroupBy}
1547
+ index={index}
1548
+ />
1549
+ ))}
1550
+ </div>
1551
+ </div>
1552
+ {isOpen && (
1553
+ <div
1554
+ ref={dropdownRef}
1555
+ role="menu"
1556
+ tabindex="0"
1557
+ data-headlessui-state="open"
1558
+ style={{
1559
+ zIndex: 120,
1560
+ position: 'absolute',
1561
+ left: '0px',
1562
+ marginTop: '12px',
1563
+ transformOrigin: 'right top',
1564
+ padding: '1rem',
1565
+ borderRadius: '0.375rem',
1566
+ backgroundColor: 'rgb(255, 255, 255)',
1567
+ boxShadow: 'rgba(0, 0, 0, 0.2) 0px 1px 5px 0px',
1568
+ }}
1569
+ // className="transform opacity-100 scale-100"
1570
+ >
1571
+ <div
1572
+ style={{
1573
+ display: 'flex',
1574
+ flexDirection: 'row',
1575
+ alignItems: 'center',
1576
+ }}
1577
+ >
1578
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
1579
+ <div
1580
+ style={{
1581
+ fontSize: '0.875rem',
1582
+ marginBottom: '6px',
1583
+ fontWeight: '600',
1584
+ color: '#384151',
1585
+ opacity: 0.7,
1586
+ }}
1587
+ >
1588
+ Column
1589
+ </div>
1590
+ {/* select column */}
1591
+ <SelectComponent
1592
+ value={selectedGroupByColumn.name}
1593
+ onChange={e => {
1594
+ const column = selectedTable.columns.find(
1595
+ c => c.name === e
1596
+ );
1597
+ setSelectedGroupByColumn(column);
1598
+ }}
1599
+ options={selectedTable.columns.map(elem => {
1600
+ return { label: elem.name, value: elem.name };
1601
+ })}
1602
+ />
1603
+ </div>
1604
+ {groupByColumnType === 'date' && (
1605
+ <div
1606
+ style={{
1607
+ display: 'flex',
1608
+ flexDirection: 'column',
1609
+ marginLeft: 12,
1610
+ }}
1611
+ >
1612
+ <div
1613
+ style={{
1614
+ fontSize: '0.875rem',
1615
+ marginBottom: '6px',
1616
+ fontWeight: '600',
1617
+ color: '#384151',
1618
+ opacity: 0.7,
1619
+ }}
1620
+ >
1621
+ Bucket
1622
+ </div>
1623
+ <SelectComponent
1624
+ value={dateBucket}
1625
+ onChange={e => {
1626
+ setDateBucket(e);
1627
+ }}
1628
+ options={[
1629
+ { label: 'month', value: 'month' },
1630
+ { label: 'day', value: 'day' },
1631
+ { label: 'week', value: 'week' },
1632
+ ]}
1633
+ />
1634
+ </div>
1635
+ )}
1636
+ </div>
1637
+ {/* Select bucket (if date) */}
1638
+
1639
+ {/* Select aggregations */}
1640
+ <div
1641
+ style={{
1642
+ fontSize: '0.875rem',
1643
+ marginBottom: '6px',
1644
+ fontWeight: '600',
1645
+ color: '#384151',
1646
+ opacity: 0.7,
1647
+ marginTop: 20,
1648
+ }}
1649
+ >
1650
+ Aggregations
1651
+ </div>
1652
+ {/* select column */}
1653
+ {aggregations.map((aggregation, index) => (
1654
+ // setAggregationType
1655
+ <div
1656
+ style={{
1657
+ display: 'flex',
1658
+ flexDirection: 'row',
1659
+ alignItems: 'center',
1660
+ }}
1661
+ >
1662
+ <SelectComponent
1663
+ value={aggregation.column?.name}
1664
+ onChange={e => {
1665
+ const column = selectedTable.columns.find(
1666
+ c => c.name === e
1667
+ );
1668
+ setAggregationColumn(column, index);
1669
+ }}
1670
+ options={selectedTable.columns.map(elem => {
1671
+ return { label: elem.name, value: elem.name };
1672
+ })}
1673
+ />
1674
+ <div style={{ width: 12 }} />
1675
+ <SelectComponent
1676
+ value={aggregation.aggregationType}
1677
+ onChange={e => {
1678
+ setAggregationType(e, index);
1679
+ }}
1680
+ options={[
1681
+ { label: 'sum', value: 'sum' },
1682
+ { label: 'average', value: 'average' },
1683
+ { label: 'count', value: 'count' },
1684
+ ]}
1685
+ />
1686
+ </div>
1687
+ ))}
1688
+ <button
1689
+ style={{
1690
+ width: '100%',
1691
+ marginTop: '1.25rem',
1692
+ paddingTop: '0.5rem',
1693
+ paddingBottom: '0.5rem',
1694
+ paddingLeft: '0.75rem',
1695
+ paddingRight: '0.75rem',
1696
+ backgroundColor: '#384151',
1697
+ // hover: { opacity: "90" },
1698
+ color: 'white',
1699
+ fontWeight: '500',
1700
+ borderRadius: '0.375rem',
1701
+ boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
1702
+ }}
1703
+ onClick={() => {
1704
+ if (groupByIndexBeingEdited > -1) {
1705
+ updateGroupBy(groupByIndexBeingEdited);
1706
+ setIsOpen(false);
1707
+ // close();
1708
+ return;
1709
+ }
1710
+ addGroupBy();
1711
+ setIsOpen(false);
1712
+ // close();
1713
+ }}
1714
+ >
1715
+ {groupByIndexBeingEdited > -1 ? 'Edit group by' : 'Add group by'}
1716
+ </button>
1717
+ </div>
1718
+ )}
1719
+ </div>
1720
+ </div>
1721
+ );
1722
+ };
1723
+
1724
+ const AddFilterModal2 = ({
1725
+ filters,
1726
+ selectedColumn,
1727
+ numberStart,
1728
+ numberEnd,
1729
+ dateStart,
1730
+ setDateStart,
1731
+ columnStats,
1732
+ stringFilterValues,
1733
+ setStringFilterValues,
1734
+ addFilter,
1735
+ setSelectedColumn,
1736
+ setNumberStart,
1737
+ setNumberEnd,
1738
+ selectedTable,
1739
+ columnType,
1740
+ setDateEnd,
1741
+ dateEnd,
1742
+ removeFilter,
1743
+ selectFilter,
1744
+ indexBeingEdited,
1745
+ updateFilter,
1746
+ SelectComponent,
1747
+ theme,
1748
+ }) => {
1749
+ const dropdownRef = useRef(null);
1750
+ const [isOpen, setIsOpen] = useState(false);
1751
+
1752
+ useEffect(() => {
1753
+ // Event listener to close the dropdown menu when clicking outside
1754
+ const handleOutsideClick = event => {
1755
+ if (
1756
+ event.target.id === 'toggle-button' ||
1757
+ event.target.id === 'filter-tag'
1758
+ ) {
1759
+ setIsOpen(isOpen => !isOpen);
1760
+ return;
1761
+ }
1762
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
1763
+ setIsOpen(false);
1764
+ }
1765
+ };
1766
+
1767
+ // Attach the event listener to the document
1768
+ document.addEventListener('click', handleOutsideClick);
1769
+
1770
+ // Clean up the event listener on component unmount
1771
+ return () => {
1772
+ document.removeEventListener('click', handleOutsideClick);
1773
+ };
1774
+ }, []);
1775
+
1776
+ return (
1777
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
1778
+ <div
1779
+ style={{
1780
+ position: 'relative',
1781
+ display: 'inline-block',
1782
+ textAlign: 'left',
1783
+ }}
1784
+ >
1785
+ <div
1786
+ style={{
1787
+ display: 'flex',
1788
+ flexDirection: 'row',
1789
+ alignItems: 'center',
1790
+ }}
1791
+ >
1792
+ <button
1793
+ id="toggle-button"
1794
+ // onClick={() => setIsOpen((isOpen) => !isOpen)}
1795
+ type="button"
1796
+ aria-haspopup="menu"
1797
+ aria-expanded="true"
1798
+ data-headlessui-state="open"
1799
+ style={{
1800
+ display: 'inline-flex',
1801
+ borderWidth: '1px',
1802
+ borderStyle: 'solid',
1803
+ borderColor: theme.borderColor,
1804
+ boxShadow: 'rgba(231, 231, 231, 0.5) 0px 1px 2px 0px',
1805
+ borderRadius: '0.375rem',
1806
+ backgroundColor: theme.backgroundColor,
1807
+ padding: '0.375rem 0.75rem',
1808
+ fontSize: '0.875rem',
1809
+ fontWeight: '500',
1810
+ color: theme.primaryTextColor,
1811
+ }}
1812
+ >
1813
+ Filters
1814
+ </button>
1815
+ <div
1816
+ style={{
1817
+ overflowX: 'scroll',
1818
+ display: 'flex',
1819
+ flexDirection: 'row',
1820
+ alignItems: 'center',
1821
+ }}
1822
+ >
1823
+ {filters.map((elem, index) => (
1824
+ <FilterTag
1825
+ id="filter-tag"
1826
+ label={elem.tag}
1827
+ removeFilter={removeFilter}
1828
+ selectFilter={selectFilter}
1829
+ index={index}
1830
+ />
1831
+ ))}
1832
+ </div>
1833
+ </div>
1834
+ {isOpen && (
1835
+ <div
1836
+ ref={dropdownRef}
1837
+ role="menu"
1838
+ tabindex="0"
1839
+ data-headlessui-state="open"
1840
+ style={{
1841
+ zIndex: 120,
1842
+ position: 'absolute',
1843
+ left: '0px',
1844
+ marginTop: '12px',
1845
+ transformOrigin: 'right top',
1846
+ padding: '1rem',
1847
+ borderRadius: '0.375rem',
1848
+ backgroundColor: 'rgb(255, 255, 255)',
1849
+ boxShadow: 'rgba(0, 0, 0, 0.2) 0px 1px 5px 0px',
1850
+ display: 'flex',
1851
+ flexDirection: 'column',
1852
+ }}
1853
+ // className="transform opacity-100 scale-100"
1854
+ >
1855
+ <div
1856
+ style={{
1857
+ fontSize: '0.875rem',
1858
+ marginBottom: '6px',
1859
+ fontWeight: '600',
1860
+ color: theme.secondaryFontColor,
1861
+ // opacity: 0.7,
1862
+ }}
1863
+ >
1864
+ Column
1865
+ </div>
1866
+ <SelectComponent
1867
+ value={selectedColumn.name}
1868
+ onChange={e => {
1869
+ const column = selectedTable.columns.find(c => c.name === e);
1870
+ setSelectedColumn(column);
1871
+ }}
1872
+ options={selectedTable.columns.map(elem => {
1873
+ return { label: elem.name, value: elem.name };
1874
+ })}
1875
+ />
1876
+
1877
+ {columnType === 'number' && (
1878
+ <div
1879
+ style={{
1880
+ width: '245px',
1881
+ maxWidth: '245px',
1882
+ zIndex: 50,
1883
+ }}
1884
+ >
1885
+ {/* <MultiRangeSlider
1886
+ style={{
1887
+ border: "none",
1888
+ boxShadow: "none",
1889
+ padding: 0,
1890
+ marginTop: 28,
1891
+ }}
1892
+ min={0}
1893
+ max={1000000}
1894
+ // step={5}
1895
+ minValue={numberStart}
1896
+ maxValue={numberEnd}
1897
+ ruler={false}
1898
+ label={false}
1899
+ preventWheel
1900
+ barLeftColor="transparent"
1901
+ barRightColor="transparent"
1902
+ barInnerColor="#384151"
1903
+ onInput={(e) => {
1904
+ setNumberStart(e.minValue);
1905
+ setNumberEnd(e.maxValue);
1906
+ }}
1907
+ /> */}
1908
+ <div
1909
+ style={{
1910
+ display: 'flex',
1911
+ flexDirection: 'row',
1912
+ alignItems: 'center',
1913
+ justifyContent: 'space-between',
1914
+ }}
1915
+ >
1916
+ <div
1917
+ style={{
1918
+ display: 'flex',
1919
+ flexDirection: 'column',
1920
+ marginTop: '20px',
1921
+ }}
1922
+ >
1923
+ <div
1924
+ style={{
1925
+ fontSize: '0.875rem',
1926
+ fontWeight: '600',
1927
+ color: '#384151',
1928
+ opacity: 0.75,
1929
+ }}
1930
+ >
1931
+ Minimum
1932
+ </div>
1933
+ <input
1934
+ // type="number"
1935
+ value={numberStart}
1936
+ onChange={e => setNumberStart(e.target.value)}
1937
+ placeholder=""
1938
+ style={{
1939
+ textAlign: 'right',
1940
+ width: '100px',
1941
+ border: '1px solid #E7E7E7',
1942
+ color: '#384151',
1943
+ marginTop: '4px',
1944
+ height: 38,
1945
+ paddingRight: 8,
1946
+ backgroundColor: 'white',
1947
+ borderRadius: '0.375rem',
1948
+ boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1)',
1949
+ }}
1950
+ />
1951
+ </div>
1952
+ <div
1953
+ style={{
1954
+ display: 'flex',
1955
+ flexDirection: 'column',
1956
+ marginTop: '20px',
1957
+ }}
1958
+ >
1959
+ <div
1960
+ style={{
1961
+ fontSize: '0.875rem',
1962
+ fontWeight: '600',
1963
+ color: '#384151',
1964
+ opacity: 0.75,
1965
+ }}
1966
+ >
1967
+ Maximum
1968
+ </div>
1969
+ <input
1970
+ // type="number"
1971
+ value={numberEnd}
1972
+ onChange={e => setNumberEnd(e.target.value)}
1973
+ placeholder=""
1974
+ style={{
1975
+ textAlign: 'right',
1976
+ width: '100px',
1977
+ border: '1px solid #E7E7E7',
1978
+ color: '#384151',
1979
+ marginTop: '4px',
1980
+ paddingRight: 8,
1981
+ height: 38,
1982
+ backgroundColor: 'white',
1983
+ borderRadius: '0.375rem',
1984
+ boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1)',
1985
+ }}
1986
+ />
1987
+ </div>
1988
+ </div>
1989
+ </div>
1990
+ )}
1991
+
1992
+ {columnType === 'date' && (
1993
+ <div
1994
+ style={{
1995
+ display: 'flex',
1996
+ flexDirection: 'row',
1997
+ alignItems: 'center',
1998
+ justifyContent: 'space-between',
1999
+ }}
2000
+ >
2001
+ <div
2002
+ style={{
2003
+ display: 'flex',
2004
+ flexDirection: 'column',
2005
+ marginTop: '20px',
2006
+ }}
2007
+ >
2008
+ <div
2009
+ style={{
2010
+ fontSize: '0.8rem',
2011
+ fontWeight: '600',
2012
+ color: 'rgba(56, 65, 81, 0.7)',
2013
+ }}
2014
+ >
2015
+ Start date
2016
+ </div>
2017
+ <input
2018
+ type="date"
2019
+ value={dateStart}
2020
+ onChange={e => setDateStart(e.target.value)}
2021
+ placeholder="Start date"
2022
+ style={{
2023
+ width: '115px',
2024
+ fontSize: '0.8rem',
2025
+ color: '#384151',
2026
+ borderWidth: '1px',
2027
+ marginTop: '4px',
2028
+ borderColor: '#E7E7E7',
2029
+ backgroundColor: 'white',
2030
+ borderRadius: '0.375rem',
2031
+ boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
2032
+ paddingLeft: '0.5rem',
2033
+ paddingRight: '0.5rem',
2034
+ paddingTop: '0.375rem',
2035
+ paddingBottom: '0.375rem',
2036
+ }}
2037
+ />
2038
+ </div>
2039
+ <div
2040
+ style={{
2041
+ display: 'flex',
2042
+ flexDirection: 'column',
2043
+ marginTop: '20px',
2044
+ }}
2045
+ >
2046
+ <div
2047
+ style={{
2048
+ fontSize: '0.8rem',
2049
+ fontWeight: '600',
2050
+ color: 'rgba(56, 65, 81, 0.7)',
2051
+ }}
2052
+ >
2053
+ End date
2054
+ </div>
2055
+ <input
2056
+ type="date"
2057
+ value={dateEnd}
2058
+ onChange={e => setDateEnd(e.target.value)}
2059
+ placeholder="End date"
2060
+ style={{
2061
+ width: '115px',
2062
+ fontSize: '0.8rem',
2063
+ color: '#384151',
2064
+ borderWidth: '1px',
2065
+ marginTop: '4px',
2066
+ borderColor: '#E7E7E7',
2067
+ backgroundColor: 'white',
2068
+ borderRadius: '0.375rem',
2069
+ boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
2070
+ paddingLeft: '0.5rem',
2071
+ paddingRight: '0.5rem',
2072
+ paddingTop: '0.375rem',
2073
+ paddingBottom: '0.375rem',
2074
+ }}
2075
+ />
2076
+ </div>
2077
+ </div>
2078
+ )}
2079
+
2080
+ {columnType === 'string' &&
2081
+ columnStats &&
2082
+ columnStats.length > 0 && (
2083
+ <div
2084
+ style={{
2085
+ flex: 'flex',
2086
+ flexDirection: 'column',
2087
+ marginTop: '14px',
2088
+ }}
2089
+ >
2090
+ {columnStats.map(value => (
2091
+ <div
2092
+ style={{
2093
+ display: 'flex',
2094
+ flexDirection: 'row',
2095
+ alignItems: 'center',
2096
+ }}
2097
+ key={value}
2098
+ >
2099
+ {/* Rest of the code */}
2100
+ <div
2101
+ style={{
2102
+ display: 'flex',
2103
+ flexDirection: 'row',
2104
+ alignItems: 'center',
2105
+ }}
2106
+ key={value}
2107
+ >
2108
+ <DivCheckbox
2109
+ // type="checkbox"
2110
+ // style={{
2111
+ // border: "1px solid red",
2112
+ // borderRadius: "4px",
2113
+ // boxSizing: "border-box",
2114
+ // color: "#fff",
2115
+ // height: "18px",
2116
+ // minWidth: "18px",
2117
+ // position: "relative",
2118
+ // width: "18px",
2119
+ // }}
2120
+ // className="border-[#E7E7E7] border-[1px] shadow-sm rounded-sm bg-white cursor-pointer"
2121
+ checked={stringFilterValues.includes(value)}
2122
+ onChange={() => {
2123
+ setStringFilterValues(prev =>
2124
+ prev.includes(value)
2125
+ ? prev.filter(v => v !== value)
2126
+ : [...prev, value]
2127
+ );
2128
+ }}
2129
+ />
2130
+ <div
2131
+ style={{
2132
+ marginLeft: 6,
2133
+ display: 'block',
2134
+ overflow: 'hidden',
2135
+ textOverflow: 'ellipsis',
2136
+ whiteSpace: 'nowrap',
2137
+ color: '#384151',
2138
+ }}
2139
+ >
2140
+ {value}
2141
+ </div>
2142
+ </div>
2143
+ </div>
2144
+ ))}
2145
+ </div>
2146
+ )}
2147
+ <button
2148
+ style={{
2149
+ width: '245px',
2150
+ marginTop: '20px',
2151
+ padding: '0.5rem 0.75rem',
2152
+ backgroundColor: 'rgb(56, 65, 81)',
2153
+ color: 'rgb(255, 255, 255)',
2154
+ fontWeight: '500',
2155
+ borderRadius: '0.375rem',
2156
+ boxShadow: 'rgba(0, 0, 0, 0.05) 0px 1px 2px 0px',
2157
+ }}
2158
+ onClick={() => {
2159
+ if (indexBeingEdited > -1) {
2160
+ updateFilter(indexBeingEdited);
2161
+ setIsOpen(false);
2162
+ return;
2163
+ }
2164
+ addFilter();
2165
+ setIsOpen(false);
2166
+ }}
2167
+ >
2168
+ {indexBeingEdited > -1 ? 'Edit filter' : 'Add filter'}
2169
+ </button>
2170
+ </div>
2171
+ )}
2172
+ </div>
2173
+ <div style={{ height: '12px' }}></div>
2174
+ </div>
2175
+ );
2176
+ };
2177
+
2178
+ const DivCheckbox = ({ onChange, checked }) => {
2179
+ const toggleCheckbox = () => {
2180
+ if (onChange) {
2181
+ onChange(!checked);
2182
+ }
2183
+ };
2184
+
2185
+ const style = {
2186
+ display: 'inline-block',
2187
+ width: '18px',
2188
+ height: '18px',
2189
+ background: checked ? '#384151' : '#fff',
2190
+ border: checked ? '1px solid #384151' : '1px solid #E7E7E7',
2191
+ borderRadius: '4px',
2192
+ position: 'relative',
2193
+ cursor: 'pointer',
2194
+ boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
2195
+ };
2196
+
2197
+ return (
2198
+ <div
2199
+ style={style}
2200
+ onClick={toggleCheckbox}
2201
+ aria-checked={checked}
2202
+ // className="shadow-sm"
2203
+ role="checkbox"
2204
+ >
2205
+ {checked && <CheckIcon className="text-white" aria-hidden="true" />}
2206
+ </div>
2207
+ );
2208
+ };