@quillsql/react 2.10.39 → 2.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/dist/cjs/Chart.d.ts +4 -0
  2. package/dist/cjs/Chart.d.ts.map +1 -1
  3. package/dist/cjs/Chart.js +5 -5
  4. package/dist/cjs/ChartBuilder.js +2 -2
  5. package/dist/cjs/Context.d.ts +1 -1
  6. package/dist/cjs/Context.d.ts.map +1 -1
  7. package/dist/cjs/Context.js +3 -1
  8. package/dist/cjs/Dashboard.d.ts +3 -1
  9. package/dist/cjs/Dashboard.d.ts.map +1 -1
  10. package/dist/cjs/Dashboard.js +4 -4
  11. package/dist/cjs/QuillProvider.d.ts +3 -1
  12. package/dist/cjs/QuillProvider.d.ts.map +1 -1
  13. package/dist/cjs/QuillProvider.js +2 -2
  14. package/dist/cjs/ReportBuilder.d.ts +40 -40
  15. package/dist/cjs/ReportBuilder.d.ts.map +1 -1
  16. package/dist/cjs/ReportBuilder.js +2036 -909
  17. package/dist/cjs/components/Chart/LineChart.d.ts +5 -1
  18. package/dist/cjs/components/Chart/LineChart.d.ts.map +1 -1
  19. package/dist/cjs/components/Chart/LineChart.js +18 -6
  20. package/dist/cjs/components/QuillTable.d.ts +1 -1
  21. package/dist/cjs/components/QuillTable.d.ts.map +1 -1
  22. package/dist/cjs/components/QuillTable.js +157 -157
  23. package/dist/cjs/components/ReportBuilder/AddColumnPopover.d.ts +2 -0
  24. package/dist/cjs/components/ReportBuilder/AddColumnPopover.d.ts.map +1 -0
  25. package/dist/cjs/components/ReportBuilder/AddColumnPopover.js +128 -0
  26. package/dist/cjs/components/ReportBuilder/ast.d.ts +512 -0
  27. package/dist/cjs/components/ReportBuilder/ast.d.ts.map +1 -0
  28. package/dist/cjs/components/ReportBuilder/ast.js +210 -0
  29. package/dist/cjs/components/ReportBuilder/bigDateMap.d.ts +7 -0
  30. package/dist/cjs/components/ReportBuilder/bigDateMap.d.ts.map +1 -0
  31. package/dist/cjs/components/ReportBuilder/bigDateMap.js +689 -0
  32. package/dist/cjs/components/ReportBuilder/constants.d.ts +89 -0
  33. package/dist/cjs/components/ReportBuilder/constants.d.ts.map +1 -0
  34. package/dist/cjs/components/ReportBuilder/constants.js +130 -0
  35. package/dist/cjs/components/ReportBuilder/convert.d.ts +41 -0
  36. package/dist/cjs/components/ReportBuilder/convert.d.ts.map +1 -0
  37. package/dist/cjs/components/ReportBuilder/convert.js +730 -0
  38. package/dist/cjs/components/ReportBuilder/operators.d.ts +445 -0
  39. package/dist/cjs/components/ReportBuilder/operators.d.ts.map +1 -0
  40. package/dist/cjs/components/ReportBuilder/operators.js +552 -0
  41. package/dist/cjs/components/ReportBuilder/pivot.d.ts +10 -0
  42. package/dist/cjs/components/ReportBuilder/pivot.d.ts.map +1 -0
  43. package/dist/cjs/components/ReportBuilder/pivot.js +2 -0
  44. package/dist/cjs/components/ReportBuilder/postgres.d.ts +150 -0
  45. package/dist/cjs/components/ReportBuilder/postgres.d.ts.map +1 -0
  46. package/dist/cjs/components/ReportBuilder/postgres.js +365 -0
  47. package/dist/cjs/components/ReportBuilder/schema.d.ts +23 -0
  48. package/dist/cjs/components/ReportBuilder/schema.d.ts.map +1 -0
  49. package/dist/cjs/components/ReportBuilder/schema.js +2 -0
  50. package/dist/cjs/components/ReportBuilder/ui.d.ts +34 -0
  51. package/dist/cjs/components/ReportBuilder/ui.d.ts.map +1 -0
  52. package/dist/cjs/components/ReportBuilder/ui.js +389 -0
  53. package/dist/cjs/components/ReportBuilder/util.d.ts +76 -0
  54. package/dist/cjs/components/ReportBuilder/util.d.ts.map +1 -0
  55. package/dist/cjs/components/ReportBuilder/util.js +648 -0
  56. package/dist/cjs/components/UiComponents.d.ts +15 -2
  57. package/dist/cjs/components/UiComponents.d.ts.map +1 -1
  58. package/dist/cjs/components/UiComponents.js +50 -3
  59. package/dist/cjs/utils/crypto.d.ts +1 -1
  60. package/dist/cjs/utils/crypto.d.ts.map +1 -1
  61. package/dist/cjs/utils/crypto.js +9 -5
  62. package/dist/esm/Chart.d.ts +4 -0
  63. package/dist/esm/Chart.d.ts.map +1 -1
  64. package/dist/esm/Chart.js +5 -5
  65. package/dist/esm/ChartBuilder.js +1 -1
  66. package/dist/esm/Context.d.ts +1 -1
  67. package/dist/esm/Context.d.ts.map +1 -1
  68. package/dist/esm/Context.js +3 -1
  69. package/dist/esm/Dashboard.d.ts +3 -1
  70. package/dist/esm/Dashboard.d.ts.map +1 -1
  71. package/dist/esm/Dashboard.js +4 -4
  72. package/dist/esm/QuillProvider.d.ts +3 -1
  73. package/dist/esm/QuillProvider.d.ts.map +1 -1
  74. package/dist/esm/QuillProvider.js +2 -2
  75. package/dist/esm/ReportBuilder.d.ts +40 -40
  76. package/dist/esm/ReportBuilder.d.ts.map +1 -1
  77. package/dist/esm/ReportBuilder.js +2038 -909
  78. package/dist/esm/components/Chart/LineChart.d.ts +5 -1
  79. package/dist/esm/components/Chart/LineChart.d.ts.map +1 -1
  80. package/dist/esm/components/Chart/LineChart.js +18 -6
  81. package/dist/esm/components/QuillTable.d.ts +1 -1
  82. package/dist/esm/components/QuillTable.d.ts.map +1 -1
  83. package/dist/esm/components/QuillTable.js +157 -157
  84. package/dist/esm/components/ReportBuilder/AddColumnPopover.d.ts +2 -0
  85. package/dist/esm/components/ReportBuilder/AddColumnPopover.d.ts.map +1 -0
  86. package/dist/esm/components/ReportBuilder/AddColumnPopover.js +125 -0
  87. package/dist/esm/components/ReportBuilder/ast.d.ts +512 -0
  88. package/dist/esm/components/ReportBuilder/ast.d.ts.map +1 -0
  89. package/dist/esm/components/ReportBuilder/ast.js +186 -0
  90. package/dist/esm/components/ReportBuilder/bigDateMap.d.ts +7 -0
  91. package/dist/esm/components/ReportBuilder/bigDateMap.d.ts.map +1 -0
  92. package/dist/esm/components/ReportBuilder/bigDateMap.js +686 -0
  93. package/dist/esm/components/ReportBuilder/constants.d.ts +89 -0
  94. package/dist/esm/components/ReportBuilder/constants.d.ts.map +1 -0
  95. package/dist/esm/components/ReportBuilder/constants.js +127 -0
  96. package/dist/esm/components/ReportBuilder/convert.d.ts +41 -0
  97. package/dist/esm/components/ReportBuilder/convert.d.ts.map +1 -0
  98. package/dist/esm/components/ReportBuilder/convert.js +719 -0
  99. package/dist/esm/components/ReportBuilder/operators.d.ts +445 -0
  100. package/dist/esm/components/ReportBuilder/operators.d.ts.map +1 -0
  101. package/dist/esm/components/ReportBuilder/operators.js +548 -0
  102. package/dist/esm/components/ReportBuilder/pivot.d.ts +10 -0
  103. package/dist/esm/components/ReportBuilder/pivot.d.ts.map +1 -0
  104. package/dist/esm/components/ReportBuilder/pivot.js +1 -0
  105. package/dist/esm/components/ReportBuilder/postgres.d.ts +150 -0
  106. package/dist/esm/components/ReportBuilder/postgres.d.ts.map +1 -0
  107. package/dist/esm/components/ReportBuilder/postgres.js +355 -0
  108. package/dist/esm/components/ReportBuilder/schema.d.ts +23 -0
  109. package/dist/esm/components/ReportBuilder/schema.d.ts.map +1 -0
  110. package/dist/esm/components/ReportBuilder/schema.js +1 -0
  111. package/dist/esm/components/ReportBuilder/ui.d.ts +34 -0
  112. package/dist/esm/components/ReportBuilder/ui.d.ts.map +1 -0
  113. package/dist/esm/components/ReportBuilder/ui.js +366 -0
  114. package/dist/esm/components/ReportBuilder/util.d.ts +76 -0
  115. package/dist/esm/components/ReportBuilder/util.d.ts.map +1 -0
  116. package/dist/esm/components/ReportBuilder/util.js +616 -0
  117. package/dist/esm/components/UiComponents.d.ts +15 -2
  118. package/dist/esm/components/UiComponents.d.ts.map +1 -1
  119. package/dist/esm/components/UiComponents.js +47 -2
  120. package/dist/esm/utils/crypto.d.ts +1 -1
  121. package/dist/esm/utils/crypto.d.ts.map +1 -1
  122. package/dist/esm/utils/crypto.js +9 -5
  123. package/package.json +1 -1
@@ -1,996 +1,2125 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // @ts-nocheck
3
- import { useState, useContext, useCallback, useEffect, useMemo, useRef, } from 'react';
4
- // import './nightOwlLight.css';
5
- import { ClientContext, SchemaContext, ThemeContext } from './Context';
6
- import { convertPostgresColumn } from './SQLEditor';
7
- import { format } from 'date-fns';
8
- import { PivotModal, generatePivotTable, } from './internals/ReportBuilder/PivotModal';
9
- import { getData, getDataFromCloud } from './utils/dataFetcher';
10
- import { getRangeFromPreset, reportBuilderOptions, } from './DateRangePicker/dateRangePickerUtils';
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useContext, useEffect, useState } from 'react';
3
+ import { MemoizedCheckbox } from './components/UiComponents';
4
+ import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, } from '@dnd-kit/core';
5
+ import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, useSortable, } from '@dnd-kit/sortable';
6
+ import { CSS as DND_CSS } from '@dnd-kit/utilities';
7
+ import { getQuarter } from 'date-fns';
8
+ import { ClientContext } from './Context';
9
+ import { getTableNames, isDateishColumnType, isNumericColumnType, isTextColumnType, } from './components/ReportBuilder/ast';
11
10
  import ChartBuilder from './ChartBuilder';
12
- import { MemoizedModal, MemoizedTextInput, MemoizedSelect, MemoizedButton, MemoizedSecondaryButton, MemoizedHeader, MemoizedLabel, MemoizedDeleteButton, MemoizedText, MemoizedPopover, } from './components/UiComponents';
13
- export default function ReportBuilder({ onChangeQuery, onChangeData, onChangeColumns, onChangeLoading, onChangePivot, onDateFilterChange, onChangeFields, onError, TextInput = MemoizedTextInput, containerStyle, Select = MemoizedSelect, Button = MemoizedButton, SecondaryButton = MemoizedSecondaryButton, Header = MemoizedHeader, Label = MemoizedLabel, DeleteButton = MemoizedDeleteButton, Text = MemoizedText, Modal = MemoizedModal, Popover = MemoizedPopover, showTableFormatOptions = false, showDateFieldOptions = false, showAccessControlOptions = false, tagStyle, tableName, dateColumn, selectedTagBorderColor, chartBuilderEnabled = false, destinationDashboard, chartBuilderTitle, chartBuilderButtonLabel, editSQLEnabled, navigateToSQLEditor, }) {
14
- const [data, setData] = useState([]);
15
- const [client, setClient] = useContext(ClientContext);
16
- const [schema, setSchema] = useContext(SchemaContext);
17
- const [theme] = useContext(ThemeContext);
18
- const [columns, setColumns] = useState([]);
19
- const [fields, setFields] = useState([]);
11
+ import { QuillButton, QuillPopover, QuillSecondaryButton, QuillSelect, QuillReportBuilderTable, QuillTabs, QuillTextInput, QuillSidebar, CustomContainer, QuillHandleButton, QuillSelectColumn, QuillDraggableColumn, QuillButtonLoadingState, QuillTableLoadingState, QuillSidebarHeading, QuillSidebarSubHeading, QuillFilterPopover, DEFAULT_TAB_OPTIONS, TagWrapper, EditPopover, AddFilterPopover, } from './components/ReportBuilder/ui';
12
+ import { generateCurrentPeriodPostgres, generateEqualsPostgres, generateLastNPeriodsPostgres, generatePreviousPeriodPostgres, } from './components/ReportBuilder/postgres';
13
+ import { applyPivot, convertBigQuery, convertGroupBy, convertRemoveSimpleParentheses, convertStringComparison, convertWildcardColumns, } from './components/ReportBuilder/convert';
14
+ import { deepCopy, formatDateComparisonNode, getDateFilterInfo, getInTheCurrentIntervalSentence, getInTheLastIntervalSentence, getInThePreviousIntervalSentence, getPostgresBasicType, isColumnComparison, isDateTruncEquals, isInTheLastInterval, isNodeEmptyCollection, isTheCurrentInterval, isThePreviousInterval, isTopLevelBoolean, showNodeAsRow, tryConvertDateEquality, } from './components/ReportBuilder/util';
15
+ import { getDefaultOperatorSubtrees, OPERATOR_GROUPS, } from './components/ReportBuilder/operators';
16
+ import { hashCode } from './utils/crypto';
17
+ import { DATE_FMT, DAY_OF_WEEK, defaultAST, defaultColumn, defaultEntry, defaultNumericComparison, defaultTable, defaultVariant, MONTH_OF_YEAR, } from './components/ReportBuilder/constants';
18
+ import AddColumnPopover from './components/ReportBuilder/AddColumnPopover';
19
+ /**
20
+ * Quill Report Builder
21
+ *
22
+ * Allows non-technical users to build SQL queries using either UI or AI and
23
+ * then edit them on the fly. Once users have constructed a query they like,
24
+ * they can click a button and add that report to their dashboard or export it
25
+ * as a CSV.
26
+ */
27
+ export default function ReportBuilder({ path = '', initialTableName = 'transactions', onAddToDashboardComplete = () => void null, destinationDashboard = undefined, dashboardItem = undefined, organizationName = '', Button = QuillButton, SecondaryButton = QuillSecondaryButton, TextInput = QuillTextInput, Select = QuillSelect, Table = QuillReportBuilderTable, Popover = QuillPopover, Tabs = QuillTabs, Checkbox = MemoizedCheckbox, Sidebar = QuillSidebar, Container = CustomContainer, HandleButton = QuillHandleButton, SelectColumn = QuillSelectColumn, DraggableColumn = QuillDraggableColumn, ButtonLoadingState = QuillButtonLoadingState, TableLoadingState = QuillTableLoadingState, SidebarHeading = QuillSidebarHeading, SidebarSubHeading = QuillSidebarSubHeading, FilterPopover = QuillFilterPopover, }) {
28
+ const [aiPrompt, setAiPrompt] = useState('');
29
+ const [errorMessage, setErrorMessage] = useState('');
30
+ const [baseAst, setBaseAst] = useState(null);
31
+ const [formData, setFormData] = useState(null);
32
+ const [orderedColumnNames, setOrderedColumnNames] = useState([]);
33
+ const [selectedColumns, setSelectedColumns] = useState([]);
34
+ const [schemaTables, setSchemaTables] = useState([]);
35
+ const [activeQuery, setActiveQuery] = useState('');
36
+ const [activeEditItem, setActiveEditItem] = useState(null);
37
+ const [activePath, setActivePath] = useState(null);
38
+ const [openPopover, setOpenPopover] = useState(null);
20
39
  const [loading, setLoading] = useState(false);
21
- const [selectedPivot, setSelectedPivot] = useState(null);
22
- const defaultDateRange = [null, null, null];
23
- const [dateRange, setDateRange] = useState(defaultDateRange);
40
+ const [isChartBuilderOpen, setIsChartBuilderOpen] = useState(false);
41
+ const [isPending, setIsPending] = useState(false);
42
+ const [isCopying, setIsCopying] = useState(false);
43
+ const [rows, setRows] = useState([]);
44
+ const [fields, setFields] = useState([]);
45
+ const [topLevelBinaryOperator, setTopLevelBinaryOperator] = useState('AND');
46
+ const [editPopoverKey, setEditPopoverKey] = useState(null);
47
+ const [uniqueValues, setUniqueValues] = useState({});
48
+ const [pivot, setPivot] = useState(null);
49
+ const [pivotData, setPivotData] = useState(null);
50
+ // eslint-disable-next-line no-unused-vars
51
+ const [client, _setClient] = useContext(ClientContext);
52
+ const enforceOrderOnColumns = (columnNames) => {
53
+ if (pivot) {
54
+ const rowName = pivot.rowField;
55
+ const sortFn = (a, b) => a === rowName ? -1 : b === rowName ? 1 : 0;
56
+ const columnsInPivot = getColumnsInPivotExpanded();
57
+ return columnNames
58
+ .sort(sortFn)
59
+ .filter((c) => columnsInPivot.includes(c));
60
+ }
61
+ return columnNames;
62
+ };
63
+ /**
64
+ * Transforms an array of column names into an array of columnInfo objects
65
+ * with label, field, format, and fieldType keys.
66
+ */
67
+ const processColumnsForChartBuilder = (columns) => {
68
+ return columns.map((col) => ({
69
+ label: col,
70
+ name: col,
71
+ displayName: col,
72
+ field: col,
73
+ format: getPostgresBasicType(fields.find((f) => f.name === col))?.replace('number', 'whole_number') || 'string',
74
+ fieldType: schemaTables
75
+ .flatMap((t) => t.columns)
76
+ .find((c) => c.name === col)?.fieldType || 'text',
77
+ }));
78
+ };
79
+ const clearAllState = () => {
80
+ // We're trying to not block the main thread while resetting all the state.
81
+ // This shouldn't be an issue since the dispatches shouldn't block, but
82
+ // this seems to work for now. ¯\_(ツ)_/¯
83
+ setTimeout(() => {
84
+ setAiPrompt('');
85
+ setBaseAst(null);
86
+ setFormData(null);
87
+ setOrderedColumnNames([]);
88
+ setSelectedColumns([]);
89
+ setSchemaTables([]);
90
+ setActiveQuery('');
91
+ setActiveEditItem(null);
92
+ setActivePath(null);
93
+ setOpenPopover(null);
94
+ setLoading(false);
95
+ setIsPending(false);
96
+ setRows([]);
97
+ setFields([]);
98
+ setTopLevelBinaryOperator('AND');
99
+ setEditPopoverKey(null);
100
+ // setUniqueValues({});
101
+ setPivot(null);
102
+ setPivotData(null);
103
+ }, 0);
104
+ };
24
105
  useEffect(() => {
25
- let isSubscribed = true;
26
- async function getClient() {
27
- try {
28
- const resp = await getDataFromCloud(client, `client/${client.publicKey}/`, null, 'GET');
29
- if (resp) {
30
- setClient({ ...client, ...resp.client });
31
- }
106
+ clearAllState();
107
+ }, [client]);
108
+ useEffect(() => {
109
+ if (activePath !== null) {
110
+ // update the modal with the new subtree
111
+ setActiveEditItem(getByKey(formData, activePath));
112
+ }
113
+ }, [formData]);
114
+ const getByKey = (formData, path) => {
115
+ if (!path)
116
+ return deepCopy(formData);
117
+ // Function to immutably update or delete nodes based on their path
118
+ const paths = path.split('.');
119
+ let current = deepCopy(formData);
120
+ for (let i = 0; i < paths.length; i++) {
121
+ if (current[paths[i]]) {
122
+ current = current[paths[i]];
32
123
  }
33
- catch (error) {
34
- console.error('ERROR getting client:', error);
124
+ }
125
+ return current;
126
+ };
127
+ const copyToClipboard = (str) => {
128
+ setIsCopying(true);
129
+ navigator.clipboard.writeText(str);
130
+ setTimeout(() => setIsCopying(false), 800);
131
+ };
132
+ const clearCheckboxes = () => {
133
+ const checkboxes = uniqueValues;
134
+ const newValues = {};
135
+ for (let table of Object.keys(checkboxes)) {
136
+ newValues[table] = {};
137
+ for (let column of Object.keys(checkboxes[table])) {
138
+ newValues[table][column] = {};
139
+ for (let variant of Object.keys(checkboxes[table][column])) {
140
+ newValues[table][column][variant] = false;
141
+ }
35
142
  }
36
143
  }
37
- if (isSubscribed) {
38
- getClient();
144
+ setUniqueValues(newValues);
145
+ };
146
+ const setCheckboxes = (node) => {
147
+ if (!['IN', 'NOT IN'].includes(node.operator))
148
+ return;
149
+ const selectedItems = node.right.value.flatMap((v) => v.args.value.map((x) => x.value.toLowerCase()));
150
+ const checkboxes = uniqueValues;
151
+ const newValues = {};
152
+ for (let table of Object.keys(checkboxes)) {
153
+ newValues[table] = {};
154
+ for (let column of Object.keys(checkboxes[table])) {
155
+ newValues[table][column] = {};
156
+ for (let variant of Object.keys(checkboxes[table][column])) {
157
+ newValues[table][column][variant] = selectedItems.includes(variant.toLowerCase());
158
+ }
159
+ }
39
160
  }
40
- return () => {
41
- isSubscribed = false;
42
- };
43
- }, []);
44
- useEffect(() => {
45
- let isSubscribed = true;
46
- async function getSchema() {
47
- const { queryEndpoint, queryHeaders, publicKey } = client;
48
- const response = await fetch(`${queryEndpoint}`, {
161
+ setUniqueValues(newValues);
162
+ };
163
+ const fetchSqlQuery = async () => {
164
+ try {
165
+ const response = await fetch(`https://quill-344421.uc.r.appspot.com/sqlify`, {
49
166
  method: 'POST',
50
167
  headers: {
51
- ...queryHeaders,
52
168
  'Content-Type': 'application/json',
53
169
  },
54
170
  body: JSON.stringify({
55
- metadata: {
56
- clientId: publicKey,
57
- publicKey: publicKey,
58
- task: 'schema',
59
- },
171
+ ast: { ...baseAst, where: formData },
172
+ publicKey: client.publicKey,
60
173
  }),
61
174
  });
62
175
  const data = await response.json();
63
- if (isSubscribed) {
64
- setSchema(data.data.tables);
65
- }
176
+ setActiveQuery(data.query);
177
+ fetchUponChange();
66
178
  }
67
- if (isSubscribed) {
68
- getSchema();
179
+ catch (error) {
180
+ console.error(error);
69
181
  }
70
- return () => {
71
- isSubscribed = false;
72
- };
73
- }, []);
182
+ };
74
183
  useEffect(() => {
75
- if (onDateFilterChange) {
76
- onDateFilterChange(dateRange);
184
+ setErrorMessage('');
185
+ if (baseAst || formData) {
186
+ fetchSqlQuery();
77
187
  }
78
- }, [dateRange]);
188
+ }, [baseAst]);
189
+ // Returns an array of all the column names in the pivot config
190
+ // if there are any, otherwise returns [].
191
+ const getColumnsInPivot = () => {
192
+ if (!pivot)
193
+ return [];
194
+ const { valueField, rowField, columnField } = pivot;
195
+ return [valueField, rowField, columnField].filter(Boolean);
196
+ };
197
+ // It's just like getColumnsInPivot but we expand the columnField
198
+ // if there is one to include all the variants just like it would
199
+ // show up in the table. (eg. category -> ...[Fuel, Food, Other])
200
+ const getColumnsInPivotExpanded = () => {
201
+ if (!pivot)
202
+ return [];
203
+ const tables = getTableNames(baseAst);
204
+ if (tables.length !== 1)
205
+ return [];
206
+ const result = [];
207
+ const table = tables[0];
208
+ const { valueField, rowField, columnField } = pivot;
209
+ if (columnField && uniqueValues[table][columnField]) {
210
+ result.push(...Object.keys(uniqueValues[table][columnField]));
211
+ }
212
+ result.push(valueField, rowField);
213
+ return result.filter(Boolean);
214
+ };
79
215
  useEffect(() => {
80
- if (onDateFilterChange) {
81
- onDateFilterChange(defaultDateRange);
216
+ if (errorMessage) {
217
+ console.error(errorMessage);
82
218
  }
83
- }, []);
219
+ }, [errorMessage]);
84
220
  useEffect(() => {
85
- setColumns([]);
86
- }, [tableName]);
87
- const runQuery = async (query) => {
88
- setLoading(true);
89
- const hostedBody = {
90
- metadata: {
91
- query,
92
- task: 'query',
93
- orgId: client.customerId || '*',
94
- databaseType: client?.databaseType,
95
- },
221
+ const fetchDistinctHelper = async (column, table) => {
222
+ try {
223
+ const query = `SELECT DISTINCT ${column} FROM ${table};`;
224
+ const response = await fetch(`https://quill-344421.uc.r.appspot.com/dashquery`, {
225
+ method: 'POST',
226
+ headers: {
227
+ 'Content-Type': 'application/json',
228
+ },
229
+ body: JSON.stringify({
230
+ orgId: 2,
231
+ publicKey: client.publicKey,
232
+ query: query,
233
+ }),
234
+ });
235
+ const data = await response.json();
236
+ if (data.errorMessage) {
237
+ console.error(data.errorMessage);
238
+ return null;
239
+ }
240
+ const options = data.rows.map((r) => r[column]);
241
+ const newCheckboxValues = options.reduce((obj, col) => {
242
+ obj[col] = false;
243
+ return obj;
244
+ }, {});
245
+ return { table, column, values: newCheckboxValues };
246
+ }
247
+ catch (e) {
248
+ console.error(e);
249
+ return null;
250
+ }
96
251
  };
97
- const cloudBody = { query };
98
- const resp = await getData(client, 'dashquery', 'same-origin', hostedBody, cloudBody);
99
- if (resp && resp.errorMessage) {
100
- onError(resp.errorMessage);
101
- setData([]);
102
- // onChangeData([]);
103
- setColumns([]);
104
- // onChangeColumns([]);
105
- setFields([]);
106
- if (onChangeFields) {
107
- onChangeFields([]);
252
+ const fetchSchema = async () => {
253
+ try {
254
+ const response = await fetch(`https://quill-344421.uc.r.appspot.com/schema2/${client.publicKey}`).then((res) => res.json());
255
+ // Filter out hidden columns on tables back from schema2.
256
+ const tables = response?.tables;
257
+ for (const table of tables) {
258
+ table.columns = table.columns.filter((column) =>
259
+ // Quick and dirty fix for removing org ids from response.
260
+ // TODO: Fix this on the backend or something.
261
+ column.isVisible && column.displayName !== 'pm_company_id');
262
+ }
263
+ setSchemaTables(tables ?? []);
264
+ setOrderedColumnNames((tables ?? [])
265
+ // .filter((t: any) => t.displayName === initialTableName)
266
+ .flatMap((table) => table.columns.map((c) => `${table.displayName}.${c.displayName}`)));
267
+ // Fetch all the unique values in parallel
268
+ const pendingFetches = [];
269
+ for (let table of tables ?? []) {
270
+ for (let column of table.columns) {
271
+ if (!isTextColumnType(column.fieldType))
272
+ continue;
273
+ const fetchPromise = fetchDistinctHelper(column.name, table.displayName);
274
+ pendingFetches.push(fetchPromise);
275
+ }
276
+ }
277
+ const newUniqueValues = {};
278
+ const resolvedPromises = await Promise.all(pendingFetches);
279
+ for (const resolvedData of resolvedPromises) {
280
+ if (resolvedData) {
281
+ const { table, column, values } = resolvedData;
282
+ if (!newUniqueValues[table]) {
283
+ newUniqueValues[table] = {};
284
+ }
285
+ newUniqueValues[table][column] = values;
286
+ }
287
+ }
288
+ if (hashCode(uniqueValues) !== hashCode(newUniqueValues)) {
289
+ setUniqueValues(newUniqueValues);
290
+ }
108
291
  }
109
- setLoading(false);
110
- return;
111
- }
112
- setData(resp.rows);
113
- setColumns(resp.fields.map((elem) => convertPostgresColumn(elem)));
114
- if (selectedPivot) {
115
- const { rows, columns } = generatePivotTable(selectedPivot, resp.rows, dateRange);
116
- if (onChangePivot) {
117
- onChangePivot(selectedPivot, columns, rows);
292
+ catch (error) {
293
+ console.error(error);
118
294
  }
295
+ };
296
+ if (schemaTables.length === 0) {
297
+ fetchSchema();
119
298
  }
120
- else {
121
- if (onChangeData) {
122
- onChangeData(resp.rows);
299
+ }, [schemaTables]);
300
+ const updateFormData = (updates, { isDeletion = false, isInsertion = false, isReplaceSubtree = false, isAddVariant = false, isDeleteVariant = false, topLevelBinOp = 'OR', isCondition = undefined, }) => {
301
+ // Function to immutably update or delete nodes based on their path
302
+ // TODO: fix the following horible code
303
+ updates.forEach(({ path, value }) => {
304
+ const globalPath = [
305
+ activePath ?? isDeletion ? path : '',
306
+ isDeletion || isReplaceSubtree ? '' : path,
307
+ ]
308
+ .filter(Boolean)
309
+ .join('.');
310
+ const paths = globalPath.split('.').filter((p) => p);
311
+ if (paths.length === 0 && !isInsertion && !isReplaceSubtree) {
312
+ setFormData(null);
313
+ setBaseAst(deepCopy({
314
+ ...defaultAST,
315
+ ...baseAst,
316
+ ...(!baseAst?.columns && {
317
+ columns: getAllPossibleColumns().map((c) => {
318
+ const newColumn = deepCopy(defaultColumn);
319
+ newColumn.expr.column = c.name;
320
+ return newColumn;
321
+ }),
322
+ }),
323
+ ...(!baseAst?.from && {
324
+ from: [{ ...defaultTable, table: initialTableName }],
325
+ }),
326
+ where: null,
327
+ }));
328
+ return;
123
329
  }
124
- if (onChangeColumns) {
125
- onChangeColumns(resp.fields.map((elem) => convertPostgresColumn(elem)));
330
+ if (!formData && isInsertion) {
331
+ setFormData(value);
332
+ setBaseAst(deepCopy({
333
+ ...defaultAST,
334
+ ...baseAst,
335
+ ...(!baseAst?.columns && {
336
+ columns: getAllPossibleColumns().map((c) => {
337
+ const newColumn = deepCopy(defaultColumn);
338
+ newColumn.expr.column = c.name;
339
+ return newColumn;
340
+ }),
341
+ }),
342
+ ...(!baseAst?.from && {
343
+ from: [{ ...defaultTable, table: initialTableName }],
344
+ }),
345
+ where: value,
346
+ }));
347
+ return;
126
348
  }
127
- }
128
- setFields(resp.fields);
129
- if (onChangeFields) {
130
- onChangeFields(resp.fields);
131
- }
132
- setLoading(false);
133
- };
134
- useEffect(() => {
135
- if (onChangeLoading) {
136
- onChangeLoading(loading);
137
- }
138
- }, [loading]);
139
- if (!schema || !schema.length || !tableName) {
140
- return null;
141
- }
142
- return (_jsx(ReportingTool, { containerStyle: containerStyle, destinationDashboard: destinationDashboard, client: client, editSQLEnabled: editSQLEnabled, navigateToSQLEditor: navigateToSQLEditor, theme: theme, data: data, columns: columns, fields: fields, chartBuilderTitle: chartBuilderTitle, chartBuilderButtonLabel: chartBuilderButtonLabel, onChangeData: onChangeData, onChangeColumns: onChangeColumns, onChangePivot: onChangePivot, selectedPivot: selectedPivot, setSelectedPivot: (pivot) => {
143
- setSelectedPivot(pivot);
144
- if (onChangePivot) {
145
- const table = pivot
146
- ? generatePivotTable(pivot, data, dateRange)
147
- : { rows: null, columns: null };
148
- onChangePivot(pivot, table.columns, table.rows);
149
- }
150
- }, schema: schema, tableName: tableName, dateColumn: dateColumn, selectedTagBorderColor: selectedTagBorderColor, onChangeQuery: onChangeQuery, runQuery: runQuery, dateRange: dateRange, setDateRange: setDateRange, defaultDateRange: defaultDateRange, chartBuilderEnabled: chartBuilderEnabled, HeaderComponent: Header, LabelComponent: Label, SelectComponent: Select, TextComponent: Text, ButtonComponent: Button, SecondaryButtonComponent: SecondaryButton, ModalComponent: Modal, PopoverComponent: Popover, TextInputComponent: TextInput, tagStyle: tagStyle, showTableFormatOptions: showTableFormatOptions, showDateFieldOptions: showDateFieldOptions, showAccessControlOptions: showAccessControlOptions }));
151
- }
152
- export function getPostgresBasicType(column) {
153
- let format;
154
- // first check if column.dataTypeID exists
155
- if (column.dataTypeID) {
156
- switch (column.dataTypeID) {
157
- case 20: // int8
158
- case 21: // int2
159
- case 23: // int4
160
- case 700: // float4
161
- case 701: // float8
162
- case 1700: // numeric
163
- format = 'number';
164
- break;
165
- case 1082: // date
166
- case 1083: // time
167
- case 1184: // timestamptz
168
- case 1114: // timestamp
169
- format = 'date';
170
- break;
171
- case 1043: // varchar
172
- default:
173
- format = 'string';
174
- }
175
- }
176
- else if (column.fieldType) {
177
- // if column.dataTypeID doesn't exist, check column.fieldType
178
- switch (column.fieldType) {
179
- case 'int8':
180
- case 'int2':
181
- case 'int4':
182
- case 'float4':
183
- case 'float8':
184
- case 'numeric':
185
- format = 'number';
186
- break;
187
- case 'date':
188
- case 'time':
189
- case 'timestamptz':
190
- case 'timestamp':
191
- format = 'date';
192
- break;
193
- case 'varchar':
194
- default:
195
- format = 'string';
196
- }
197
- }
198
- return format;
199
- }
200
- const newDateWhereAST = (column, dateRange, databaseType) => {
201
- // all time means no filter
202
- if (dateRange[2] === 'at') {
203
- return null;
204
- }
205
- // if using preset
206
- if (dateRange[2]) {
207
- const timeInterval = reportBuilderOptions.find((elem) => elem.value === dateRange[2])?.dayInterval;
208
- switch (databaseType) {
209
- case 'BigQuery': {
210
- return {
349
+ let newState = deepCopy(formData);
350
+ let current = newState;
351
+ let parent = null;
352
+ let parentKey = null;
353
+ for (let i = 0; i < paths.length - 1; i++) {
354
+ if (current[paths[i]]) {
355
+ parent = current;
356
+ parentKey = paths[i];
357
+ current = current[paths[i]];
358
+ }
359
+ }
360
+ const lastKey = paths[paths.length - 1];
361
+ if (isDeletion) {
362
+ if (lastKey === 'left' || lastKey === 'right') {
363
+ if (parent) {
364
+ if (lastKey === 'right') {
365
+ parent[parentKey] = parent[parentKey].left;
366
+ }
367
+ else {
368
+ parent[parentKey] = parent[parentKey].right;
369
+ }
370
+ }
371
+ else {
372
+ delete current[lastKey];
373
+ if (newState?.left && !newState?.right) {
374
+ newState = newState.left;
375
+ }
376
+ else if (newState?.right && !newState?.left) {
377
+ newState = newState.right;
378
+ }
379
+ }
380
+ }
381
+ }
382
+ else if (isInsertion) {
383
+ newState = {
211
384
  type: 'binary_expr',
212
- operator: 'BETWEEN',
213
- left: {
214
- type: 'function',
215
- name: 'Date',
216
- args: {
217
- type: 'expr_list',
218
- value: [
219
- {
220
- type: 'column_ref',
221
- table: null,
222
- column: column,
223
- },
224
- ],
225
- },
226
- over: null,
227
- },
228
- right: {
229
- type: 'expr_list',
230
- value: [
231
- {
232
- type: 'function',
233
- name: 'DATE_SUB',
234
- args: {
235
- type: 'expr_list',
236
- value: [
237
- {
238
- type: 'function',
239
- name: 'CURRENT_DATE',
240
- args: {
241
- type: 'expr_list',
242
- value: [],
243
- },
244
- over: null,
245
- },
246
- {
247
- type: 'interval',
248
- expr: {
249
- type: 'number',
250
- value: timeInterval,
251
- },
252
- unit: 'day',
253
- },
254
- ],
255
- },
256
- over: null,
257
- },
258
- {
259
- type: 'function',
260
- name: 'CURRENT_DATE',
261
- args: {
262
- type: 'expr_list',
263
- value: [],
264
- },
265
- over: null,
266
- },
267
- ],
268
- },
385
+ operator: topLevelBinOp,
386
+ isCondition: isCondition,
387
+ left: newState,
388
+ right: value,
269
389
  };
270
390
  }
271
- default: {
272
- return {
391
+ else if (isAddVariant) {
392
+ const newVariant = deepCopy(defaultVariant);
393
+ if (value) {
394
+ newVariant.args.value[0].value = value;
395
+ // if there is already a single default value there,
396
+ // let's remove it so when we push we replace.
397
+ if (current[lastKey].length === 1 &&
398
+ current[lastKey][0].args.value[0].value === '') {
399
+ current[lastKey].pop();
400
+ }
401
+ }
402
+ current[lastKey].push(newVariant);
403
+ }
404
+ else if (isDeleteVariant) {
405
+ if (value) {
406
+ const argList = current[lastKey];
407
+ argList.splice(argList.findIndex((arg) => arg.args.value[0].value === name), 1);
408
+ }
409
+ else {
410
+ current[lastKey].pop();
411
+ }
412
+ }
413
+ else if (isReplaceSubtree) {
414
+ if (lastKey) {
415
+ current[lastKey] = value;
416
+ }
417
+ else {
418
+ newState = value;
419
+ }
420
+ }
421
+ else {
422
+ if (typeof current[lastKey] === 'object' && current[lastKey] !== null) {
423
+ current[lastKey].value = value;
424
+ }
425
+ else {
426
+ current[lastKey] = value;
427
+ }
428
+ }
429
+ setFormData(newState);
430
+ setBaseAst({
431
+ ...defaultAST,
432
+ ...baseAst,
433
+ ...(!baseAst?.columns && {
434
+ columns: getAllPossibleColumns().map((c) => {
435
+ const newColumn = deepCopy(defaultColumn);
436
+ newColumn.expr.column = c.name;
437
+ return newColumn;
438
+ }),
439
+ }),
440
+ ...(!baseAst?.from && {
441
+ from: [{ ...defaultTable, table: initialTableName }],
442
+ }),
443
+ where: { ...newState },
444
+ });
445
+ });
446
+ };
447
+ // TODO: Merge this function with the updateFormData function
448
+ const updateActiveItem = (updates, { isDeletion = false, isInsertion = false, isReplaceSubtree = false, isAddVariant = false, isDeleteVariant = false, column = undefined, }) => {
449
+ let newState = deepCopy(activeEditItem);
450
+ updates.forEach(({ path, value }) => {
451
+ let current = newState;
452
+ const globalPath = path;
453
+ const paths = globalPath.split('.').filter((p) => p);
454
+ if (paths.length === 0 && !isInsertion && !isReplaceSubtree) {
455
+ setActiveEditItem(null);
456
+ return;
457
+ }
458
+ const isOperatorChange = paths[paths.length - 1] === 'operator';
459
+ let parent = null;
460
+ let parentKey = null;
461
+ for (let i = 0; i < paths.length - 1; i++) {
462
+ let currentPath = paths[i];
463
+ let index;
464
+ if (paths[i].includes('||')) {
465
+ const splitPath = paths[i].split('||');
466
+ currentPath = splitPath[0];
467
+ index = splitPath[1];
468
+ }
469
+ if (current[currentPath]) {
470
+ parent = current;
471
+ parentKey = currentPath;
472
+ current = current[currentPath];
473
+ }
474
+ if (index) {
475
+ current = current[parseInt(index)];
476
+ }
477
+ }
478
+ const lastKey = paths[paths.length - 1];
479
+ if (isDeletion) {
480
+ if (lastKey === 'left' || lastKey === 'right') {
481
+ if (parent) {
482
+ if (lastKey === 'right') {
483
+ parent[parentKey] = parent[parentKey].left;
484
+ }
485
+ else {
486
+ parent[parentKey] = parent[parentKey].right;
487
+ }
488
+ }
489
+ else {
490
+ delete current[lastKey];
491
+ if (newState?.left && !newState?.right) {
492
+ newState = newState.left;
493
+ }
494
+ else if (newState?.right && !newState?.left) {
495
+ newState = newState.right;
496
+ }
497
+ }
498
+ }
499
+ }
500
+ else if (isInsertion) {
501
+ const columns = getAllPossibleColumns();
502
+ const defaultColumn = columns[0].name;
503
+ // TODO: I think this is a bug, take a closer look here
504
+ newState = {
273
505
  type: 'binary_expr',
274
506
  operator: 'AND',
275
- left: {
276
- type: 'binary_expr',
277
- operator: '>=',
278
- left: {
279
- type: 'column_ref',
280
- table: null,
281
- column: column,
282
- },
283
- right: {
284
- type: 'binary_expr',
285
- operator: '-',
286
- left: {
287
- type: 'function',
288
- name: 'now',
289
- args: {
290
- type: 'expr_list',
291
- value: [],
292
- },
293
- },
294
- right: {
295
- type: 'interval',
296
- expr: {
297
- type: 'single_quote_string',
298
- value: `${timeInterval} day`,
299
- },
300
- unit: '',
301
- },
302
- },
303
- },
507
+ left: newState,
304
508
  right: {
305
- type: 'binary_expr',
306
- operator: '<=',
509
+ ...defaultEntry,
307
510
  left: {
308
- type: 'column_ref',
309
- table: null,
310
- column: column,
311
- },
312
- right: {
313
- type: 'function',
314
- name: 'now',
315
- args: {
316
- type: 'expr_list',
317
- value: [],
318
- },
511
+ ...defaultEntry.left,
512
+ column: defaultColumn,
319
513
  },
320
514
  },
321
515
  };
322
516
  }
323
- }
324
- }
325
- else {
326
- return {
327
- type: 'binary_expr',
328
- operator: 'BETWEEN',
329
- left: {
330
- type: 'column_ref',
331
- table: null,
332
- column: column,
333
- },
334
- right: {
335
- type: 'expr_list',
336
- value: [
337
- {
338
- type: 'single_quote_string',
339
- value: format(new Date(dateRange[0]), 'MM/dd/yyyy'),
340
- },
341
- {
342
- type: 'single_quote_string',
343
- value: format(new Date(dateRange[1]), 'MM/dd/yyyy'),
344
- },
345
- ],
346
- },
347
- };
348
- }
349
- };
350
- function ReportingTool({ schema, data, columns, runQuery, SelectComponent, ButtonComponent, SecondaryButtonComponent, editSQLEnabled, navigateToSQLEditor, onChangeQuery, onChangePivot, selectedPivot, setSelectedPivot, theme, ModalComponent, HeaderComponent, PopoverComponent, TextComponent, TextInputComponent, LabelComponent, tagStyle, tableName, dateColumn, selectedTagBorderColor, dateRange, setDateRange, defaultDateRange, chartBuilderEnabled, fields, containerStyle, showTableFormatOptions = false, showDateFieldOptions = false, showAccessControlOptions = false, client, destinationDashboard, chartBuilderTitle, chartBuilderButtonLabel, }) {
351
- const selectedTable = useMemo(() => schema.find((t) => t.displayName === tableName), [schema, tableName]);
352
- const [selectedColumn, setSelectedColumn] = useState(schema[0].columns.find((elem) => elem.name !== 'id'));
353
- const parentRef = useRef();
354
- const [filters, setFilters] = useState([]);
355
- const [AST, setAST] = useState({
356
- with: null,
357
- type: 'select',
358
- options: null,
359
- distinct: { type: null },
360
- columns: '*',
361
- into: { position: null },
362
- from: [{ db: null, table: selectedTable.displayName, as: null }],
363
- // where: newDateWhereAST(dateColumn, defaultDateRange),
364
- where: null,
365
- groupby: null,
366
- having: null,
367
- orderby: null,
368
- limit: { seperator: '', value: [] },
369
- window: null,
370
- });
371
- const [ASTNoDateColumn, setASTNoDateColumn] = useState(AST);
372
- const [numberStart, setNumberStart] = useState(0);
373
- const [numberEnd, setNumberEnd] = useState(0);
374
- const [filterDateRange, setFilterDateRange] = useState(getRangeFromPreset('90d'));
375
- const [computedColumns, setComputedColumns] = useState({});
376
- const [stringFilterValues, setStringFilterValues] = useState([]);
377
- const [columnType, setColumnType] = useState(getPostgresBasicType(schema[0].columns[0]));
378
- const [sqlQuery, setSqlQuery] = useState('');
379
- const [sqlQueryNoDateColumn, setSqlQueryNoDateColumn] = useState('');
380
- const [indexBeingEdited, setIndexBeingEdited] = useState(-1);
381
- const [isAddFilterModalOpen, setIsAddFilterModalOpen] = useState(false);
382
- const [isEdittingPivot, setIsEdittingPivot] = useState(false);
383
- const [isPivotModalOpen, setIsPivotModalOpen] = useState(false);
384
- const [isChartBuilderOpen, setIsChartBuilderOpen] = useState(false);
385
- const [selectedPivotIndex, setSelectedPivotIndex] = useState(-1);
386
- const [createdPivots, setCreatedPivots] = useState([]);
387
- const [recommendedPivots, setRecommendedPivots] = useState([]);
388
- const [pivotRowField, setPivotRowField] = useState(undefined);
389
- const [pivotColumnField, setPivotColumnField] = useState(undefined);
390
- const [pivotValueField, setPivotValueField] = useState(undefined);
391
- const [pivotAggregation, setPivotAggregation] = useState(undefined);
392
- const [pivotPopUpTitle, setPivotPopUpTitle] = useState('Add Pivot');
393
- useEffect(() => {
394
- setColumnType(getPostgresBasicType(selectedColumn));
395
- }, [selectedColumn]);
396
- useEffect(() => {
397
- removePivot();
398
- setCreatedPivots([]);
399
- setRecommendedPivots([]);
400
- }, [selectedTable]);
401
- const selectFilter = (index) => {
402
- const filter = filters[index];
403
- const matchingColumn = selectedTable.columns.find((column) => column.name === filter.column);
404
- if (indexBeingEdited === index) {
405
- setIndexBeingEdited(-1);
406
- setStringFilterValues([]);
407
- setNumberStart(0);
408
- setNumberEnd(0);
409
- setFilterDateRange(getRangeFromPreset('90d'));
410
- return;
411
- }
412
- setSelectedColumn(matchingColumn);
413
- if (filter.columnType === 'string') {
414
- setStringFilterValues(filter.stringFilterValues);
415
- setIndexBeingEdited(index);
416
- }
417
- else if (filter.columnType === 'number') {
418
- setNumberStart(filter.numberStart);
419
- setNumberEnd(filter.numberEnd);
420
- setIndexBeingEdited(index);
421
- }
422
- else if (filter.columnType === 'date') {
423
- setFilterDateRange(filter.filterDateRange);
424
- setIndexBeingEdited(index);
425
- }
517
+ else if (isAddVariant) {
518
+ const newVariant = deepCopy(defaultVariant);
519
+ if (value) {
520
+ newVariant.args.value[0].value = value;
521
+ // if there is already a single default value there,
522
+ // let's remove it so when we push we replace.
523
+ if (current[lastKey].length === 1 &&
524
+ current[lastKey][0].args.value[0].value === '') {
525
+ current[lastKey].pop();
526
+ }
527
+ }
528
+ current[lastKey].push(newVariant);
529
+ }
530
+ else if (isDeleteVariant) {
531
+ if (value) {
532
+ const argList = current[lastKey];
533
+ argList.splice(argList.findIndex((arg) => arg.args.value[0].value === value), 1);
534
+ }
535
+ else {
536
+ current[lastKey].pop();
537
+ }
538
+ // add back in a phantom element to prevent app from crashing
539
+ // when the user removes all variants and hits save.
540
+ if (current[lastKey].length === 0) {
541
+ const newVariant = deepCopy(defaultVariant);
542
+ newVariant.args.value[0].value = '';
543
+ current[lastKey].push(newVariant);
544
+ }
545
+ }
546
+ else if (isReplaceSubtree) {
547
+ if (lastKey) {
548
+ current[lastKey] = value;
549
+ }
550
+ else {
551
+ newState = value;
552
+ }
553
+ }
554
+ else if (isOperatorChange) {
555
+ const newOp = value;
556
+ const oldOp = current[lastKey];
557
+ if (OPERATOR_GROUPS[oldOp] === OPERATOR_GROUPS[newOp]) {
558
+ current[lastKey] = value;
559
+ }
560
+ else {
561
+ const group = OPERATOR_GROUPS[newOp];
562
+ const subtree = getDefaultOperatorSubtrees(group, newOp, column, '', client.databaseType);
563
+ if (parentKey) {
564
+ parent[parentKey] = deepCopy(subtree);
565
+ }
566
+ else {
567
+ newState = deepCopy(subtree);
568
+ }
569
+ }
570
+ }
571
+ else {
572
+ if (typeof current[lastKey] === 'object' && current[lastKey] !== null) {
573
+ current[lastKey].value = value;
574
+ }
575
+ else {
576
+ current[lastKey] = value;
577
+ }
578
+ }
579
+ });
580
+ // Function to immutably update or delete nodes based on their path
581
+ setActiveEditItem(newState);
582
+ };
583
+ const handleChange = (updates) => {
584
+ const callback = isPending ? updateActiveItem : updateFormData;
585
+ callback(updates, {});
586
+ };
587
+ const handleChangeText = (updates) => {
588
+ const callback = isPending ? updateActiveItem : updateFormData;
589
+ callback(updates, {});
426
590
  };
427
- const selectedPivotTable = useMemo(() => {
428
- if (selectedPivot && data) {
429
- return generatePivotTable(selectedPivot, data, dateRange);
591
+ // Function to handle operator changes
592
+ const handleOperatorChange = (value, node, keyPrefix, column = null) => {
593
+ if (isPending) {
594
+ updateActiveItem([{ path: keyPrefix + 'operator', value }], { column });
430
595
  }
431
596
  else {
432
- return {};
433
- }
434
- }, [selectedPivot, data, dateRange]);
435
- const removePivot = () => {
436
- setSelectedPivotIndex(-1);
437
- setSelectedPivot(null);
438
- if (onChangePivot) {
439
- onChangePivot(null, null, null);
597
+ updateFormData([{ path: keyPrefix + 'operator', value }], { column });
440
598
  }
441
599
  };
442
- const selectPivot = (pivot, index) => {
443
- setSelectedPivotIndex(index);
444
- setSelectedPivot(pivot);
445
- const pivotTable = generatePivotTable(pivot, data, dateRange);
446
- if (onChangePivot) {
447
- onChangePivot(pivot, pivotTable.columns, pivotTable.rows);
448
- }
600
+ // Function to replace an entire subtree with a given value.
601
+ const handleReplaceSubtree = (keyPrefix, newValue, pending = isPending) => {
602
+ const callback = pending ? updateActiveItem : updateFormData;
603
+ callback([{ path: keyPrefix, value: newValue }], {
604
+ isReplaceSubtree: true,
605
+ });
449
606
  };
450
- const updateFilter = (index) => {
451
- if (selectedColumn && columnType) {
452
- if (columnType === 'string') {
453
- setFilters((filters) => {
454
- const newFilters = [...filters];
455
- newFilters[index] = {
456
- column: selectedColumn.name,
457
- columnType,
458
- stringFilterValues,
459
- tag: `${selectedColumn.name} (${stringFilterValues.join(', ')})`,
460
- };
461
- return newFilters;
462
- });
463
- }
464
- else if (columnType === 'number') {
465
- setFilters((filters) => {
466
- const newFilters = [...filters];
467
- newFilters[index] = {
468
- column: selectedColumn.name,
469
- columnType,
470
- numberStart,
471
- numberEnd,
472
- tag: `${numberStart} < ${selectedColumn.name} < ${numberEnd}`,
473
- };
474
- return newFilters;
475
- });
476
- }
477
- else if (columnType === 'date') {
478
- const label = filterDateRange[2]
479
- ? reportBuilderOptions.find((elem) => elem.value === filterDateRange[2])?.text
480
- : `${format(new Date(filterDateRange[0]), 'MMM dd')} - ${format(new Date(filterDateRange[1]), 'MMM dd')}`;
481
- setFilters((filters) => {
482
- const newFilters = [...filters];
483
- newFilters[index] = {
484
- column: selectedColumn.name,
485
- columnType,
486
- filterDateRange,
487
- tag: `${selectedColumn.name} (${label})`,
488
- };
489
- return newFilters;
490
- });
491
- }
492
- setStringFilterValues([]);
493
- setNumberStart(0);
494
- setNumberEnd(0);
495
- setFilterDateRange(getRangeFromPreset('90d'));
496
- setIndexBeingEdited(-1);
497
- return;
498
- }
607
+ // Function to handle the deletion of expressions
608
+ const handleDelete = (key) => {
609
+ updateFormData([{ path: path + key, value: null }], { isDeletion: true });
499
610
  };
500
- // ADD FILTER TO "FILTERS" ARRAY
501
- const addFilter = async () => {
502
- if (selectedColumn && columnType) {
503
- // const type = getPostgresBasicType(selectedColumn);
504
- let newCondition;
505
- if (columnType === 'string') {
506
- setFilters((filters) => {
507
- return [
508
- ...filters,
509
- {
510
- column: selectedColumn.name,
511
- columnType,
512
- stringFilterValues,
513
- tag: `${selectedColumn.name} (${stringFilterValues.join(', ')})`,
514
- },
611
+ // Function to handle the insertion of expressions
612
+ const handleInsertion = (value, op = 'OR', isCondition = undefined) => {
613
+ updateFormData([{ path, value }], {
614
+ isInsertion: true,
615
+ topLevelBinOp: op,
616
+ isCondition,
617
+ });
618
+ };
619
+ // Function to handle the insertion of expr_list variants
620
+ const handleInsertVariant = (key, name = null) => {
621
+ // note: if name, treat that as the name of the value to insert
622
+ const callback = isPending ? updateActiveItem : updateFormData;
623
+ callback([{ path: path + key, value: name }], { isAddVariant: true });
624
+ };
625
+ // Function to handle the insertion of expr_list variants
626
+ const handleDeleteVariant = (key, name = null) => {
627
+ // note: if name, treat that as the name of the valeu to delete
628
+ const callback = isPending ? updateActiveItem : updateFormData;
629
+ callback([{ path: path + key, value: name }], { isDeleteVariant: true });
630
+ };
631
+ const getColumnValueForColumnComparison = (node) => node.left.value ??
632
+ node.left.column ??
633
+ node.left.args?.value[0]?.value ??
634
+ node.left.args?.value[0]?.column ??
635
+ undefined;
636
+ /**
637
+ * Searches for the column by name and returns the field type.
638
+ *
639
+ * Searches the known schema and returns the fieldType of the first column
640
+ * it can find with the given name. Will first search through the current
641
+ * list of fields in the current query if any, then will default to searching
642
+ * through the whole schema.
643
+ *
644
+ * If more than one column exist with the given name, it will return the first
645
+ * one that it finds. This might not be the one that you intended.
646
+ *
647
+ * TODO: pass an optional table param to limit the search to a given table.
648
+ *
649
+ * @param columnName the name to search for.
650
+ * @returns the fieldType string or undefined if not found.
651
+ */
652
+ const getColumnTypeByName = (columnName) => {
653
+ const field = fields.find((f) => f.name === columnName);
654
+ if (field)
655
+ return getPostgresBasicType(field);
656
+ const column = schemaTables
657
+ .flatMap((t) => t.columns)
658
+ .find((c) => c.name === columnName);
659
+ return column?.fieldType;
660
+ };
661
+ /**
662
+ * Render form fields based on the type of the node
663
+ * @param node the AST or subtree to render recursively
664
+ * @param keyPrefix a stringified version of the path from the root
665
+ *
666
+ * Note: The keyPrefix should be separated by '.' characters and each item
667
+ * should be a valid index into the node (eg. 'left.right.value' is a valid
668
+ * keyPrefix but 'left.args[0].value' is not -- should be 'left.args.0.value')
669
+ */
670
+ const renderNode = (node, keyPrefix = '') => {
671
+ const dateComparisonPartialMatch = formatDateComparisonNode(node);
672
+ switch (node.type) {
673
+ case 'binary_expr':
674
+ if (dateComparisonPartialMatch ||
675
+ (isDateTruncEquals(node) && client.databaseType !== 'BigQuery')) {
676
+ const { dateColumn,
677
+ // see onChange callback handleChange
678
+ // eslint-disable-next-line no-unused-vars
679
+ dateColumnPath, dateFilterType, intervalCount, intervalType, intervalPaths, } = getDateFilterInfo(node);
680
+ const isPlural = intervalCount && intervalCount > 1 ? 's' : '';
681
+ // Pull off the string literal date for "equals" comparisons
682
+ const rawDateStringEquals = node.right?.value ??
683
+ node.right?.args?.value[1]?.column ??
684
+ node.right?.args?.value[1]?.value;
685
+ const rawDateStringEqualsPath = (node.right?.value && 'node.right.value') ??
686
+ (node.right?.args?.value[1]?.column &&
687
+ 'node.right.args.value.1.column') ??
688
+ (node.right?.args?.value[1]?.value &&
689
+ 'node.right.args.value.1.value');
690
+ return (_jsxs("div", { style: { display: 'flex', gap: 20 }, children: [_jsx(Select, { value: dateColumn, onChange: (value) => {
691
+ const columnType = getColumnTypeByName(value);
692
+ if (isDateishColumnType(columnType)) {
693
+ // handleChange(value, keyPrefix + dateColumnPath, "text");
694
+ handleOperatorChange('IN_THE_LAST', node, keyPrefix, value);
695
+ }
696
+ else if (isNumericColumnType(columnType)) {
697
+ const newSubtree = deepCopy(defaultNumericComparison);
698
+ newSubtree.left.column = value;
699
+ handleReplaceSubtree(keyPrefix, newSubtree);
700
+ }
701
+ else {
702
+ const newSubtree = deepCopy(defaultEntry);
703
+ newSubtree.left.args.value[0].column = value;
704
+ handleReplaceSubtree(keyPrefix, newSubtree);
705
+ }
706
+ }, options: getAllPossibleColumns().map((column) => ({
707
+ label: column.displayName,
708
+ value: column.name,
709
+ })) }), _jsx(Select, { value: dateFilterType, onChange: (value) => {
710
+ if (value === dateFilterType)
711
+ return null;
712
+ let newSubtree = {};
713
+ // TODO: implement one for each database type (eg. pg, snowflake, etc.)
714
+ if (value === 'in the last') {
715
+ newSubtree = generateLastNPeriodsPostgres({
716
+ dateField: dateColumn,
717
+ intervalPeriod: `${intervalCount ?? 1} ${intervalType}`,
718
+ });
719
+ }
720
+ else if (value === 'in the previous') {
721
+ newSubtree = generatePreviousPeriodPostgres({
722
+ dateField: dateColumn,
723
+ intervalPeriod: `${intervalCount ?? 1} ${intervalType}`,
724
+ currentPeriod: intervalType,
725
+ });
726
+ }
727
+ else if (value === 'in the current') {
728
+ newSubtree = generateCurrentPeriodPostgres({
729
+ dateField: dateColumn,
730
+ currentPeriod: intervalType,
731
+ });
732
+ }
733
+ else if (value === 'equals') {
734
+ newSubtree = generateEqualsPostgres({
735
+ dateField: dateColumn,
736
+ currentPeriod: intervalType,
737
+ timestamp: '2024-01-01',
738
+ });
739
+ }
740
+ // replace the entire subtree for this filter
741
+ handleReplaceSubtree(keyPrefix, newSubtree);
742
+ }, options: [
743
+ { label: 'in the last', value: 'in the last' },
744
+ { label: 'in the previous', value: 'in the previous' },
745
+ { label: 'in the current', value: 'in the current' },
746
+ { label: 'equals', value: 'equals' },
747
+ ] }), !['in the current', 'equals'].includes(dateFilterType) && (_jsx(TextInput, { value: intervalCount, type: "number", style: { width: '70px' }, onChange: (e) => {
748
+ const isPluralNow = e.target.value > 1 ? 's' : '';
749
+ intervalPaths.forEach((intervalPath) => handleChangeText([
750
+ {
751
+ value: `${e.target.value} ${intervalType}${isPluralNow}`,
752
+ path: keyPrefix + intervalPath,
753
+ },
754
+ ]));
755
+ } })), _jsx(Select, { value: intervalType, onChange: (value) => {
756
+ if (intervalPaths.length === 1) {
757
+ handleChangeText([
758
+ {
759
+ value: intervalCount !== null
760
+ ? `${intervalCount} ${value}${isPlural}`
761
+ : value,
762
+ path: keyPrefix + intervalPaths[0],
763
+ },
764
+ ]);
765
+ return;
766
+ }
767
+ let newSubtree;
768
+ if (dateFilterType === 'equals') {
769
+ newSubtree = generateEqualsPostgres({
770
+ dateField: dateColumn,
771
+ currentPeriod: value,
772
+ timestamp: rawDateStringEquals,
773
+ });
774
+ }
775
+ else {
776
+ newSubtree = generateCurrentPeriodPostgres({
777
+ dateField: dateColumn,
778
+ currentPeriod: value,
779
+ });
780
+ }
781
+ handleReplaceSubtree(keyPrefix, newSubtree);
782
+ }, options: [
783
+ { label: `year${isPlural}`, value: 'year' },
784
+ { label: `quarter${isPlural}`, value: 'quarter' },
785
+ { label: `month${isPlural}`, value: 'month' },
786
+ { label: `week${isPlural}`, value: 'week' },
787
+ { label: `day${isPlural}`, value: 'day' },
788
+ { label: `hour${isPlural}`, value: 'hour' },
789
+ ] }), dateFilterType === 'equals' && (_jsx(TextInput, { value: rawDateStringEquals, type: "text", style: { width: '120px' }, onChange: (e) => {
790
+ handleChangeText([
791
+ {
792
+ value: e.target.value,
793
+ path: keyPrefix + rawDateStringEqualsPath,
794
+ },
795
+ ]);
796
+ } }))] }));
797
+ }
798
+ else if (isInTheLastInterval(node, client.databaseType)) {
799
+ const { dateColumn, dateFilterType, intervalCount, intervalType } = getDateFilterInfo(node);
800
+ const options = getAllPossibleColumns().map((column) => ({
801
+ label: column.displayName,
802
+ value: column.name,
803
+ }));
804
+ const plural = node.right.args.value[1].expr.value > 1 ? 's' : '';
805
+ return (_jsxs("div", { style: {
806
+ display: 'flex',
807
+ flexDirection: 'row',
808
+ alignItems: 'center',
809
+ gap: 20,
810
+ }, children: [_jsx("div", { children: _jsx(Select, { value: node.left.column, onChange: (value) => {
811
+ const columnType = getColumnTypeByName(value);
812
+ if (isDateishColumnType(columnType)) {
813
+ // handleChange(value, keyPrefix + dateColumnPath, "text");
814
+ handleOperatorChange('IN_THE_LAST', node, keyPrefix, value);
815
+ }
816
+ else if (isNumericColumnType(columnType)) {
817
+ const newSubtree = deepCopy(defaultNumericComparison);
818
+ newSubtree.left.column = value;
819
+ handleReplaceSubtree(keyPrefix, newSubtree);
820
+ }
821
+ else {
822
+ const newSubtree = deepCopy(defaultEntry);
823
+ newSubtree.left.args.value[0].column = value;
824
+ handleReplaceSubtree(keyPrefix, newSubtree);
825
+ }
826
+ }, options: options }) }), _jsx(Select, { value: dateFilterType, onChange: (value) => {
827
+ handleOperatorChange(value, node, keyPrefix, dateColumn);
828
+ }, options: [
829
+ { label: 'in the last', value: 'IN_THE_LAST' },
830
+ { label: 'in the previous', value: 'IN_THE_PREVIOUS' },
831
+ { label: 'in the current', value: 'IN_THE_CURRENT' },
832
+ // { label: 'equals', value: 'equals' },
833
+ ] }), _jsx(TextInput, { defaultValue: node.right.args.value[1].expr.value, type: "number", style: { width: '120px', height: '42px' }, min: 0, onBlur: (e) => {
834
+ handleChange([
835
+ {
836
+ value: e.target.value,
837
+ path: keyPrefix + 'right.args.value||1.expr.value',
838
+ },
839
+ ]);
840
+ } }), _jsx("div", { children: _jsx(Select, { value: node.right.args.value[1].unit, onChange: (value) => handleChange([
841
+ { value, path: keyPrefix + 'right.args.value||1.unit' },
842
+ ]), options: [
843
+ { label: `year${plural}`, value: '* 365 DAY' },
844
+ { label: `month${plural}`, value: '* 30 DAY' },
845
+ { label: `week${plural}`, value: '* 7 DAY' },
846
+ { label: `day${plural}`, value: 'DAY' },
847
+ ] }) })] }));
848
+ }
849
+ else if (isTheCurrentInterval(node, client.databaseType)) {
850
+ const { dateFilterType } = getDateFilterInfo(node);
851
+ const options = getAllPossibleColumns().map((column) => ({
852
+ label: column.displayName,
853
+ value: column.name,
854
+ }));
855
+ return (_jsxs("div", { style: {
856
+ display: 'flex',
857
+ flexDirection: 'row',
858
+ alignItems: 'center',
859
+ gap: 20,
860
+ }, children: [_jsx(Select, { value: node.left.column, onChange: (value) => {
861
+ const columnType = getColumnTypeByName(value);
862
+ if (isDateishColumnType(columnType)) {
863
+ // handleChange(value, keyPrefix + dateColumnPath, "text");
864
+ handleOperatorChange('IN_THE_LAST', node, keyPrefix, value);
865
+ }
866
+ else if (isNumericColumnType(columnType)) {
867
+ const newSubtree = deepCopy(defaultNumericComparison);
868
+ newSubtree.left.column = value;
869
+ handleReplaceSubtree(keyPrefix, newSubtree);
870
+ }
871
+ else {
872
+ const newSubtree = deepCopy(defaultEntry);
873
+ newSubtree.left.args.value[0].column = value;
874
+ handleReplaceSubtree(keyPrefix, newSubtree);
875
+ }
876
+ }, options: options }), _jsx(Select, { value: 'IN_THE_CURRENT', onChange: (value) => {
877
+ handleOperatorChange(value, node, keyPrefix, node.left.column);
878
+ }, options: [
879
+ { label: 'in the last', value: 'IN_THE_LAST' },
880
+ { label: 'in the previous', value: 'IN_THE_PREVIOUS' },
881
+ { label: 'in the current', value: 'IN_THE_CURRENT' },
882
+ // { label: 'equals', value: 'equals' },
883
+ ] }), _jsx(Select, { value: node.left.args.value[1].column, onChange: (value) => {
884
+ handleChange([
885
+ { value, path: 'right.args.value||1.column' },
886
+ { value, path: 'left.args.value||1.column' },
887
+ ]);
888
+ }, options: [
889
+ { label: `year`, value: 'YEAR' },
890
+ { label: `quarter`, value: 'QUARTER' },
891
+ { label: `month`, value: 'MONTH' },
892
+ { label: `week`, value: 'WEEK' },
893
+ ] })] }));
894
+ }
895
+ else if (isThePreviousInterval(node, client.databaseType)) {
896
+ const options = getAllPossibleColumns().map((column) => ({
897
+ label: column.displayName,
898
+ value: column.name,
899
+ }));
900
+ return (_jsxs("div", { style: {
901
+ display: 'flex',
902
+ flexDirection: 'row',
903
+ alignItems: 'center',
904
+ gap: 20,
905
+ }, children: [_jsx(Select, { value: node.left.column, onChange: (value) => {
906
+ const columnType = getColumnTypeByName(value);
907
+ if (isDateishColumnType(columnType)) {
908
+ // handleChange(value, keyPrefix + dateColumnPath, "text");
909
+ handleOperatorChange('IN_THE_LAST', node, keyPrefix, value);
910
+ }
911
+ else if (isNumericColumnType(columnType)) {
912
+ const newSubtree = deepCopy(defaultNumericComparison);
913
+ newSubtree.left.column = value;
914
+ handleReplaceSubtree(keyPrefix, newSubtree);
915
+ }
916
+ else {
917
+ const newSubtree = deepCopy(defaultEntry);
918
+ newSubtree.left.args.value[0].column = value;
919
+ handleReplaceSubtree(keyPrefix, newSubtree);
920
+ }
921
+ }, opt: true, options: options }), _jsx(Select, { value: 'IN_THE_PREVIOUS', onChange: (value) => {
922
+ handleOperatorChange(value, node, keyPrefix, node.left.column);
923
+ }, options: [
924
+ { label: 'in the last', value: 'IN_THE_LAST' },
925
+ { label: 'in the previous', value: 'IN_THE_PREVIOUS' },
926
+ { label: 'in the current', value: 'IN_THE_CURRENT' },
927
+ // { label: 'equals', value: 'equals' },
928
+ ] }), _jsx(Select, { value: node.left.args.value[1].column, onChange: (value) => {
929
+ const dayConversion = {
930
+ YEAR: 365,
931
+ QUARTER: 90,
932
+ MONTH: 30,
933
+ WEEK: 7,
934
+ };
935
+ handleChange([
936
+ {
937
+ value,
938
+ path: 'left.args.value||1.column',
939
+ },
940
+ {
941
+ value,
942
+ path: 'right.args.value||1.column',
943
+ },
944
+ {
945
+ value: dayConversion[value] || 30,
946
+ path: 'right.args.value||0.args.value||1.expr.value',
947
+ },
948
+ ]);
949
+ }, options: [
950
+ { label: `year`, value: 'YEAR' },
951
+ { label: `quarter`, value: 'QUARTER' },
952
+ { label: `month`, value: 'MONTH' },
953
+ { label: `week`, value: 'WEEK' },
954
+ ] })] }));
955
+ }
956
+ else if (isColumnComparison(node)) {
957
+ const options = getAllPossibleColumns().map((column) => ({
958
+ label: column.displayName,
959
+ value: column.name,
960
+ }));
961
+ // grab the value of the left child of the column comparison
962
+ // operator (ie. the column name)
963
+ const leftChildValue = getColumnValueForColumnComparison(node);
964
+ const column = schemaTables
965
+ .flatMap((t) => t.columns)
966
+ .find((col) => col.name === leftChildValue);
967
+ const tables = getTableNames(baseAst);
968
+ const table = tables.length === 1 ? tables[0] : initialTableName;
969
+ const columnType = column?.fieldType;
970
+ const operatorOptions = [
971
+ ...(isNumericColumnType(columnType)
972
+ ? [
973
+ { label: 'equal to', value: '=' },
974
+ { label: 'not equal to', value: '!=' },
975
+ { label: 'greater than', value: '>' },
976
+ { label: 'less than', value: '<' },
977
+ { label: 'greater than or equal to', value: '>=' },
978
+ { label: 'less than or equal to', value: '<=' },
979
+ { label: 'is not null', value: 'IS NOT' },
980
+ { label: 'is null', value: 'IS' },
981
+ ]
982
+ : []),
983
+ ...(isTextColumnType(columnType)
984
+ ? [
985
+ { label: 'is', value: 'LIKE' },
986
+ { label: 'is not', value: 'NOT LIKE' },
987
+ { label: 'is one of', value: 'IN' },
988
+ { label: 'is not one of', value: 'NOT IN' },
989
+ { label: 'is not null', value: 'IS NOT' },
990
+ { label: 'is null', value: 'IS' },
991
+ ]
992
+ : []),
993
+ ...(isDateishColumnType(columnType)
994
+ ? [
995
+ { label: 'in the last', value: 'IN_THE_LAST' },
996
+ {
997
+ label: 'in the previous',
998
+ value: 'IN_THE_PREVIOUS',
999
+ },
1000
+ { label: 'in the current', value: 'IN_THE_CURRENT' },
1001
+ { label: 'equals', value: 'equals' },
1002
+ { label: 'is not null', value: 'IS NOT' },
1003
+ { label: 'is null', value: 'IS' },
1004
+ ]
1005
+ : []),
515
1006
  ];
516
- });
517
- setStringFilterValues([]);
518
- setNumberStart(0);
519
- setNumberEnd(0);
520
- setFilterDateRange(getRangeFromPreset('90d'));
521
- return;
1007
+ return (_jsxs("div", { style: {
1008
+ display: 'flex',
1009
+ gap: 12,
1010
+ flexDirection: 'column',
1011
+ width: '100%',
1012
+ }, children: [_jsxs("div", { style: {
1013
+ display: 'flex',
1014
+ gap: 20,
1015
+ // justifyContent: "space-between",
1016
+ flexDirection: showNodeAsRow(node, formData)
1017
+ ? 'row'
1018
+ : 'column',
1019
+ width: '100%',
1020
+ }, children: [_jsx(Select, { style: { width: 'min-content' }, value: leftChildValue, onChange: (value) => {
1021
+ const columnType = getColumnTypeByName(value);
1022
+ if (isDateishColumnType(columnType)) {
1023
+ handleOperatorChange('IN_THE_LAST', node, keyPrefix, value);
1024
+ }
1025
+ else if (isNumericColumnType(columnType)) {
1026
+ const newSubtree = deepCopy(defaultNumericComparison);
1027
+ newSubtree.left.column = value;
1028
+ handleReplaceSubtree(keyPrefix, newSubtree);
1029
+ }
1030
+ else {
1031
+ const newSubtree = deepCopy(defaultEntry);
1032
+ newSubtree.left.args.value[0].column = value;
1033
+ handleReplaceSubtree(keyPrefix, newSubtree);
1034
+ }
1035
+ }, options: options }), operatorOptions.length > 0 && (_jsx(Select, { value: node.operator, onChange: (value) => {
1036
+ handleOperatorChange(value, node, keyPrefix, leftChildValue);
1037
+ }, style: { width: 'min-content' }, options: operatorOptions })), node.right &&
1038
+ node.right.type !== 'expr_list' &&
1039
+ renderNode(node.right, keyPrefix + 'right.')] }, keyPrefix), node.right && node.right.type === 'expr_list' && (_jsx("div", { style: {
1040
+ display: 'grid',
1041
+ gridTemplateColumns: 'repeat(2, 1fr)',
1042
+ gap: 12,
1043
+ }, children: Object.keys(uniqueValues[table][leftChildValue] ?? {}).map((key) => (_jsxs("label", { style: { display: 'flex', gap: 2 }, children: [_jsx(Checkbox, { checked: uniqueValues[table][leftChildValue][key], onChange: (checked) => {
1044
+ const newValues = deepCopy(uniqueValues);
1045
+ newValues[table][leftChildValue][key] = checked;
1046
+ setUniqueValues(newValues);
1047
+ if (checked) {
1048
+ handleInsertVariant(keyPrefix + 'right.' + 'value', key);
1049
+ }
1050
+ else {
1051
+ handleDeleteVariant(keyPrefix + 'right.' + 'value', key);
1052
+ }
1053
+ } }), _jsx("span", { children: key })] }, key))) }, keyPrefix + 'right.'))] }));
1054
+ }
1055
+ else {
1056
+ const columnName = node.left.column;
1057
+ const column = schemaTables[0]?.columns.find((col) => col.name === columnName);
1058
+ const columnType = column?.fieldType;
1059
+ return (_jsxs("div", { style: {
1060
+ display: 'flex',
1061
+ gap: 12,
1062
+ justifyContent: 'space-between',
1063
+ flexDirection: showNodeAsRow(node, formData) ? 'row' : 'column',
1064
+ width: '100%',
1065
+ }, children: [node.left && renderNode(node.left, keyPrefix + 'left.'), _jsx(Select, { value: node.operator, onChange: (value) => {
1066
+ handleOperatorChange(value, node, keyPrefix);
1067
+ }, style: { width: `100%` }, options: [
1068
+ // { label: `and`, value: "AND" },
1069
+ // { label: `or`, value: "OR" },
1070
+ ...(isNumericColumnType(columnType)
1071
+ ? [
1072
+ { label: 'equal to', value: '=' },
1073
+ { label: 'not equal to', value: '!=' },
1074
+ { label: 'greater than', value: '>' },
1075
+ { label: 'less than', value: '<' },
1076
+ { label: 'greater than or equal to', value: '>=' },
1077
+ { label: 'less than or equal to', value: '<=' },
1078
+ { label: 'is not null', value: 'IS NOT' },
1079
+ { label: 'is null', value: 'IS' },
1080
+ ]
1081
+ : []),
1082
+ ...(isTextColumnType(columnType)
1083
+ ? [
1084
+ { label: 'is', value: 'LIKE' },
1085
+ { label: 'is not', value: 'NOT LIKE' },
1086
+ { label: 'is one of', value: 'IN' },
1087
+ { label: 'is not one of', value: 'NOT IN' },
1088
+ { label: 'is not null', value: 'IS NOT' },
1089
+ { label: 'is null', value: 'IS' },
1090
+ ]
1091
+ : []),
1092
+ ...(isDateishColumnType(columnType)
1093
+ ? [
1094
+ { label: 'in the last', value: 'IN_THE_LAST' },
1095
+ { label: 'in the previous', value: 'IN_THE_PREVIOUS' },
1096
+ { label: 'in the current', value: 'IN_THE_CURRENT' },
1097
+ { label: 'is not null', value: 'IS NOT' },
1098
+ { label: 'is null', value: 'IS' },
1099
+ ]
1100
+ : []),
1101
+ // { label: `minus`, value: "-" },
1102
+ // { label: `plus`, value: "+" },
1103
+ ] }), node.right && renderNode(node.right, keyPrefix + 'right.')] }, keyPrefix));
1104
+ }
1105
+ case 'column_ref': {
1106
+ const options = getAllPossibleColumns().map((column) => ({
1107
+ label: column.displayName,
1108
+ value: column.name,
1109
+ }));
1110
+ return (_jsx(Select, { style: { width: '120px' }, value: node.column ?? options[0]?.value, onChange: (value) => {
1111
+ handleChange([{ value, path: keyPrefix + 'column' }]);
1112
+ }, options: options }));
522
1113
  }
523
- else if (columnType === 'number') {
524
- setFilters((filters) => {
525
- return [
526
- ...filters,
527
- {
528
- column: selectedColumn.name,
529
- columnType,
530
- numberStart,
531
- numberEnd,
532
- tag: `${numberStart} < ${selectedColumn.name} < ${numberEnd}`,
533
- },
534
- ];
535
- });
536
- setStringFilterValues([]);
537
- setNumberStart(0);
538
- setNumberEnd(0);
539
- setFilterDateRange(getRangeFromPreset('90d'));
540
- return;
1114
+ case 'expr_list': {
1115
+ const len = node.value.length;
1116
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'row', gap: 12 }, children: [node.value.map((elem, index) => {
1117
+ if (elem.value) {
1118
+ return (_jsx(TextInput, { type: elem.type === 'number' ? 'number' : 'text', defaultValue: elem.value, onBlur: (e) => handleChange([
1119
+ {
1120
+ value: elem.type === 'number'
1121
+ ? parseFloat(e.target.value)
1122
+ : e.target.value,
1123
+ path: keyPrefix + `value.${index}.`,
1124
+ },
1125
+ ]) }, `input_${index}`));
1126
+ }
1127
+ return renderNode(elem, keyPrefix + `value.${index}.`);
1128
+ }), len > 1 && (_jsx(SecondaryButton, { onClick: () => handleDeleteVariant(keyPrefix + 'value'), children: "-" })), _jsx(SecondaryButton, { onClick: () => handleInsertVariant(keyPrefix + 'value'), children: "+" })] }, keyPrefix));
541
1129
  }
542
- else if (columnType === 'date') {
543
- const label = filterDateRange[2]
544
- ? reportBuilderOptions.find((elem) => elem.value === filterDateRange[2])?.text
545
- : `${format(new Date(filterDateRange[0]), 'MMM dd')} - ${format(new Date(filterDateRange[1]), 'MMM dd')}`;
546
- setFilters((filters) => {
547
- return [
548
- ...filters,
1130
+ case 'double_quote_string':
1131
+ case 'single_quote_string':
1132
+ return (_jsx(TextInput, { type: "text", defaultValue: node.value.replaceAll('%', ''), style: { width: '120px' }, onBlur: (e) => handleChange([
549
1133
  {
550
- column: selectedColumn.name,
551
- columnType,
552
- filterDateRange,
553
- tag: `${selectedColumn.name} (${label})`,
1134
+ value: node.type === 'number'
1135
+ ? parseFloat(e.target.value)
1136
+ : e.target.value,
1137
+ path: keyPrefix + 'value',
554
1138
  },
555
- ];
556
- });
557
- setStringFilterValues([]);
558
- setNumberStart(0);
559
- setNumberEnd(0);
560
- setFilterDateRange(getRangeFromPreset('90d'));
561
- }
1139
+ ]) }));
1140
+ case 'null':
1141
+ return _jsx("div", {});
1142
+ case 'number':
1143
+ return (_jsx(TextInput, { defaultValue: node.value, type: "number", style: { width: '120px' }, onBlur: (e) => {
1144
+ handleChange([
1145
+ {
1146
+ value: node.type === 'number'
1147
+ ? parseFloat(e.target.value)
1148
+ : e.target.value,
1149
+ path: keyPrefix + 'value',
1150
+ },
1151
+ ]);
1152
+ } }));
1153
+ case 'function':
1154
+ if (!node.args) {
1155
+ return _jsx("label", {});
1156
+ }
1157
+ else if (node.args.type === 'expr_list' &&
1158
+ node.args.value.length === 1) {
1159
+ return (_jsx("div", { style: { display: 'flex', flexDirection: 'row' }, children: node.args.value[0] &&
1160
+ renderNode(node.args.value[0], keyPrefix + 'args.value.0.') }));
1161
+ }
1162
+ else if (node.args.type === 'expr_list' &&
1163
+ node.args.value.length === 2) {
1164
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'row', gap: 20 }, children: [node.args.value[0] &&
1165
+ renderNode(node.args.value[0], keyPrefix + 'args.value.0.'), node.args.value[1] &&
1166
+ renderNode(node.args.value[1], keyPrefix + 'args.value.1.')] }));
1167
+ }
1168
+ return node.name;
1169
+ case 'interval':
1170
+ return (_jsx("div", { style: { display: 'flex', flexDirection: 'row', gap: 20 }, children: renderNode(node.expr, keyPrefix + 'expr.') }));
1171
+ default:
1172
+ return null;
562
1173
  }
563
1174
  };
564
- const handleRunQuery = (newSqlQuery) => {
565
- const queryToUse = newSqlQuery || sqlQuery;
566
- if (queryToUse && queryToUse.length) {
567
- runQuery(queryToUse);
1175
+ const renderSentence = (formData, node, keyPrefix = '',
1176
+ // @depreciated TODO: remove next update
1177
+ // eslint-disable-next-line no-unused-vars
1178
+ isTopLevel = false,
1179
+ // @depreciated TODO: remove next update
1180
+ // eslint-disable-next-line no-unused-vars
1181
+ isTopLevelAnd = false, isParentRow = false) => {
1182
+ const isInTheCurrentIntervalSentence = isTheCurrentInterval(node, client.databaseType)
1183
+ ? getInTheCurrentIntervalSentence(node, client.databaseType)
1184
+ : null;
1185
+ const isInTheLastIntervalSentence = isInTheLastInterval(node, client.databaseType)
1186
+ ? getInTheLastIntervalSentence(node, client.databaseType)
1187
+ : null;
1188
+ const isInThePreviousIntervalSentence = isThePreviousInterval(node, client.databaseType)
1189
+ ? getInThePreviousIntervalSentence(node, client.databaseType)
1190
+ : null;
1191
+ let dateComparisonPartialMatch = null;
1192
+ let dateEqualityPartialMatch = null;
1193
+ if (client.databaseType !== 'BigQuery') {
1194
+ dateComparisonPartialMatch = formatDateComparisonNode(node);
1195
+ dateEqualityPartialMatch = tryConvertDateEquality(node, client.databaseType);
1196
+ }
1197
+ const isRow = !isTopLevelBoolean(node);
1198
+ const isCard = isRow && !isParentRow;
1199
+ const OPS = {
1200
+ AND: 'and',
1201
+ OR: 'or',
1202
+ LIKE: 'is',
1203
+ BETWEEN: 'is between',
1204
+ IN: 'is one of',
1205
+ 'NOT IN': 'is not one of',
1206
+ 'NOT LIKE': 'is not',
1207
+ '!=': 'is not',
1208
+ '=': 'is',
1209
+ '<': 'is less than',
1210
+ '>': 'is greater than',
1211
+ '<=': 'is less than or equal to',
1212
+ '>=': 'is greater than or equal to',
1213
+ '<>': 'is not',
1214
+ '-': 'minus',
1215
+ 'IS NOT': 'is not',
1216
+ IS: 'is ',
1217
+ };
1218
+ switch (node.type) {
1219
+ case 'binary_expr':
1220
+ return (_jsx(TagWrapper, { keyPrefix: keyPrefix, formData: formData, activeEditItem: activeEditItem, setEditPopoverKey: setEditPopoverKey, setActiveEditItem: setActiveEditItem, setCheckboxes: setCheckboxes, handleReplaceSubtree: handleReplaceSubtree, FilterPopover: FilterPopover, setActivePath: setActivePath, setOpenPopover: setOpenPopover, setIsPending: setIsPending, clearCheckboxes: clearCheckboxes, fetchSqlQuery: fetchSqlQuery, handleDelete: handleDelete, editPopoverKey: editPopoverKey, isCard: isCard, isRow: isRow, getByKey: getByKey, node: node, EditPopover: EditPopover, Button: Button, renderNode: renderNode, Popover: Popover, style: {
1221
+ display: 'flex',
1222
+ gap: 3,
1223
+ flexDirection: isRow ? 'row' : 'column',
1224
+ padding: '1px',
1225
+ border: isCard ? '1px solid black' : 'none',
1226
+ whiteSpace: 'nowrap',
1227
+ overflow: 'hidden',
1228
+ textOverflow: 'ellipsis',
1229
+ }, children: dateComparisonPartialMatch ??
1230
+ dateEqualityPartialMatch ??
1231
+ isInTheCurrentIntervalSentence ??
1232
+ isInTheLastIntervalSentence ??
1233
+ isInThePreviousIntervalSentence ?? (_jsxs(_Fragment, { children: [node.left &&
1234
+ renderSentence(formData, node.left, keyPrefix + 'left.', false, false, isRow), isRow ? (' ' + OPS[node.operator] + ' ') : topLevelBinaryOperator === 'OR' ? (_jsx(TopLevelBooleanSwitch, { node: node, keyPrefix: keyPrefix, handleOperatorChange: handleOperatorChange, Select: Select })) : null, node.right &&
1235
+ renderSentence(formData, node.right, keyPrefix + 'right.', false, false, isRow)] })) }));
1236
+ case 'column_ref':
1237
+ return node.column;
1238
+ case 'expr_list':
1239
+ if (node.value.length === 1) {
1240
+ const subQuery = renderSentence(formData, node.value[0]);
1241
+ if (subQuery) {
1242
+ return `${subQuery}`;
1243
+ }
1244
+ return '()';
1245
+ }
1246
+ return `${node.value
1247
+ .map((elem) => renderSentence(formData, elem))
1248
+ .join(', ')}`;
1249
+ case 'single_quote_string':
1250
+ return node.value.replaceAll('%', '');
1251
+ case 'double_quote_string':
1252
+ case 'number':
1253
+ return node.value;
1254
+ case 'null':
1255
+ return 'null';
1256
+ case 'interval':
1257
+ if (node.unit) {
1258
+ // eg. `INTERVAL '90' DAY` -> "90 days"
1259
+ return `${node.expr.value} ${node.unit}s`;
1260
+ }
1261
+ return node.expr.value;
1262
+ case 'function':
1263
+ if (node.name.toLowerCase() === 'lower' ||
1264
+ node.name.toLowerCase() === 'upper') {
1265
+ // how many transactions were above 20 and not fuel or made in the last 90 days (hint: use lower)
1266
+ if (node.args.value.length < 1)
1267
+ return null;
1268
+ if (node.args.value[0].value) {
1269
+ return node.args.value[0].value.replaceAll('%', '');
1270
+ }
1271
+ if (node.args.value[0].column)
1272
+ return node.args.value[0].column.replaceAll('%', '');
1273
+ return null;
1274
+ }
1275
+ if (node.name.toLowerCase() === 'current_date' ||
1276
+ node.name.toLowerCase() === 'now') {
1277
+ return 'today';
1278
+ }
1279
+ if (node.name.toLowerCase() === 'date_trunc') {
1280
+ // eg. date_trunc('month', now())
1281
+ if (!node.args)
1282
+ return null;
1283
+ if (node.args.type !== 'expr_list')
1284
+ return null;
1285
+ if (node.args.value?.length !== 2)
1286
+ return null;
1287
+ const interval = renderSentence(formData, node.args.value[0]);
1288
+ const timestamp = renderSentence(formData, node.args.value[1]);
1289
+ return `start of the ${interval} of ${timestamp}`;
1290
+ }
1291
+ return null;
1292
+ default:
1293
+ return null;
568
1294
  }
569
1295
  };
570
- useEffect(() => {
571
- // if selected table changes, clear everything
572
- if (selectedTable.displayName !== AST.from.table) {
573
- setSelectedColumn(selectedTable.columns.find((elem) => elem.name !== 'id'));
574
- setFilters([]);
575
- removePivot();
576
- const resetAST = {
577
- with: null,
578
- type: 'select',
579
- options: null,
580
- distinct: { type: null },
581
- columns: '*',
582
- into: { position: null },
583
- // where: newDateWhereAST(dateColumn, defaultDateRange),
584
- where: null,
585
- groupby: null,
586
- having: null,
587
- orderby: null,
588
- limit: { seperator: '', value: [] },
589
- window: null,
590
- from: [{ db: null, table: selectedTable.displayName, as: null }],
591
- };
592
- setAST(resetAST);
593
- setASTNoDateColumn(resetAST);
594
- return;
1296
+ const getAllPossibleColumns = () => {
1297
+ if (!baseAst || !baseAst.from) {
1298
+ return schemaTables.flatMap((table) => table.columns.map((c) => ({
1299
+ ...c,
1300
+ table: table.displayName,
1301
+ })));
595
1302
  }
596
- }, [selectedTable]);
597
- const generateNewAST = (includeDateColumn) => {
598
- if (filters.length || dateRange) {
599
- const newAST = {
600
- with: null,
601
- type: 'select',
602
- options: null,
603
- distinct: null,
604
- columns: '*',
605
- into: { position: null },
606
- from: [{ db: null, table: selectedTable.displayName, as: null }],
607
- // where: newDateWhereAST(dateColumn, dateRange || defaultDateRange),
608
- where: null,
609
- groupby: null,
610
- having: null,
611
- orderby: null,
612
- limit: null,
613
- window: null,
614
- };
615
- // FILTERS
616
- for (let i = 0; i < filters.length; i++) {
617
- const filter = filters[i];
618
- const { column, columnType, stringFilterValues, numberStart, numberEnd, filterDateRange, } = filter;
619
- let newCondition;
620
- if (column === dateColumn && !includeDateColumn) {
621
- continue;
622
- }
623
- if (columnType === 'string') {
624
- newCondition = {
625
- type: 'binary_expr',
626
- operator: 'IN',
627
- left: {
628
- type: 'column_ref',
629
- table: null,
630
- column: column,
631
- },
632
- right: {
633
- type: 'expr_list',
634
- value: stringFilterValues.map((value) => ({
635
- type: 'single_quote_string',
636
- value,
637
- })),
638
- },
639
- };
640
- }
641
- else if (columnType === 'number') {
642
- newCondition = {
643
- type: 'binary_expr',
644
- operator: 'BETWEEN',
645
- left: {
646
- type: 'column_ref',
647
- table: null,
648
- column: column,
649
- },
650
- right: {
651
- type: 'expr_list',
652
- value: [
653
- { type: 'number', value: numberStart },
654
- { type: 'number', value: numberEnd },
655
- ],
656
- },
657
- };
1303
+ // TODO: support infinitely nested FROM table lookups.
1304
+ // This currently only supports top-level table names in the FROM section
1305
+ // of queries (eg. FROM "table_name", not "FROM (SELECT * FROM other) AS table_name")
1306
+ const tableNamesInQuery = baseAst.from.map((tbl) => tbl.table);
1307
+ return schemaTables
1308
+ .filter((t) => tableNamesInQuery.includes(t.displayName))
1309
+ .flatMap((table) => table.columns.map((c) => ({
1310
+ ...c,
1311
+ table: table.displayName,
1312
+ })));
1313
+ };
1314
+ const getDateColumns = () => {
1315
+ const allColumns = getAllPossibleColumns();
1316
+ return allColumns.filter((c) => isDateishColumnType(c.fieldType));
1317
+ };
1318
+ const getNumericColumns = () => {
1319
+ const allColumns = getAllPossibleColumns();
1320
+ const selectedColumnNames = selectedColumns.map((col) => col.split('.')[1]);
1321
+ return allColumns
1322
+ .filter((column) => {
1323
+ return selectedColumnNames.includes(column.name);
1324
+ })
1325
+ .filter((c) => isNumericColumnType(c.fieldType));
1326
+ };
1327
+ const getNonNumericColumns = () => {
1328
+ const allColumns = getAllPossibleColumns();
1329
+ const selectedColumnNames = selectedColumns.map((col) => col.split('.')[1]);
1330
+ return allColumns
1331
+ .filter((column) => selectedColumnNames.includes(column.name))
1332
+ .filter((c) => !isNumericColumnType(c.fieldType));
1333
+ };
1334
+ const getStringColumns = () => {
1335
+ const allColumns = getAllPossibleColumns();
1336
+ const selectedColumnNames = selectedColumns.map((col) => col.split('.')[1]);
1337
+ return allColumns
1338
+ .filter((column) => selectedColumnNames.includes(column.name))
1339
+ .filter((c) => isTextColumnType(c.fieldType));
1340
+ };
1341
+ /**
1342
+ * Return whether all columns have been selected (used to hide select all
1343
+ * and show clear button).
1344
+ */
1345
+ const isSelectedAllColumns = () => {
1346
+ if (selectedColumns.length < 1)
1347
+ return false;
1348
+ const allColumns = orderedColumnNames.filter((row) => {
1349
+ const [table, _] = row.split('.');
1350
+ return selectedColumns[0].startsWith(table);
1351
+ });
1352
+ return selectedColumns.length === allColumns.length;
1353
+ };
1354
+ const nameToColumn = (name) => ({
1355
+ type: 'expr',
1356
+ expr: {
1357
+ type: 'column_ref',
1358
+ table: null,
1359
+ column: name,
1360
+ },
1361
+ as: null,
1362
+ });
1363
+ const SortableItem = ({ id, label, setSelectedColumns, selectedColumns, }) => {
1364
+ const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: id });
1365
+ const style = {
1366
+ transform: DND_CSS.Transform.toString(transform),
1367
+ transition,
1368
+ };
1369
+ const handleSelect = () => {
1370
+ setSelectedColumns((selectedColumns) => {
1371
+ if (selectedColumns.includes(id)) {
1372
+ return selectedColumns.filter((column) => column !== id);
658
1373
  }
659
- else if (columnType === 'date') {
660
- newCondition = newDateWhereAST(column, filterDateRange, client.databaseType);
1374
+ else {
1375
+ return [...selectedColumns, id];
661
1376
  }
662
- if (!newAST.where) {
663
- newAST.where = newCondition;
1377
+ });
1378
+ };
1379
+ return (_jsx("div", { style: { userSelect: 'none', ...style }, ref: setNodeRef, children: _jsx(SelectColumn, { selected: selectedColumns?.includes(id), setSelected: handleSelect, label: label, children: _jsx("div", { style: {
1380
+ cursor: 'grab',
1381
+ }, ...attributes, ...listeners, children: _jsx(HandleButton, {}) }) }) }));
1382
+ };
1383
+ const AddConditionPopover = ({ onSave }) => {
1384
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 2 }, children: [_jsx("h1", { style: {
1385
+ fontWeight: '600',
1386
+ fontSize: 18,
1387
+ margin: 0,
1388
+ textAlign: 'left',
1389
+ }, children: "Add condition" }), _jsx(Tabs, { defaultValue: topLevelBinaryOperator, options: DEFAULT_TAB_OPTIONS, onValueChange: (value) => setTopLevelBinaryOperator(value) }), activeEditItem && renderNode(activeEditItem), _jsx("div", { style: {
1390
+ display: 'flex',
1391
+ flexDirection: 'row',
1392
+ gap: 8,
1393
+ justifyContent: 'end',
1394
+ }, children: _jsx(Button, { onMouseUp: onSave, children: "Add condition" }) })] }));
1395
+ };
1396
+ const fetchUponChange = async () => {
1397
+ if ((formData || baseAst) && !loading) {
1398
+ try {
1399
+ setLoading(true);
1400
+ const res2 = await fetch('https://quill-344421.uc.r.appspot.com/patterns', {
1401
+ method: 'POST',
1402
+ headers: {
1403
+ 'Content-Type': 'application/json',
1404
+ },
1405
+ body: JSON.stringify({
1406
+ ast: { ...baseAst, where: formData },
1407
+ publicKey: client.publicKey,
1408
+ orgId: '2',
1409
+ }),
1410
+ });
1411
+ const data2 = await res2.json();
1412
+ if (data2.rows && data2.rows.length) {
1413
+ const tables = getTableNames(baseAst);
1414
+ const table = tables.length >= 1 ? tables[0] : initialTableName;
1415
+ if (pivot) {
1416
+ // Do all of this to make sure we have the right unique columns when applying a pivot
1417
+ let uniqueFormatted = {};
1418
+ const uniqueRecords = Array.from(new Set(data2.rows.map((row) => row[pivot.columnField]))).reduce((acc, curr) => {
1419
+ acc[curr] = false;
1420
+ return acc;
1421
+ }, {});
1422
+ uniqueFormatted[pivot.columnField] = uniqueRecords;
1423
+ const pivotedData = applyPivot(data2, pivot, uniqueFormatted);
1424
+ console.info(`%c[Pivot]: ${JSON.stringify(pivot)}`, 'color: dimgray');
1425
+ setPivotData(pivotedData);
1426
+ }
1427
+ else {
1428
+ setRows(data2.rows);
1429
+ setFields(data2.fields);
1430
+ if (data2.errorMessage) {
1431
+ setErrorMessage(`Error: ${data2.errorMessage}`);
1432
+ }
1433
+ }
664
1434
  }
665
1435
  else {
666
- newAST.where = {
667
- type: 'binary_expr',
668
- operator: 'AND',
669
- left: newAST.where,
670
- right: newCondition,
671
- };
1436
+ setRows([]);
1437
+ setFields([]);
672
1438
  }
673
1439
  }
674
- return newAST;
1440
+ catch (e) {
1441
+ console.error(e);
1442
+ }
1443
+ finally {
1444
+ setLoading(false);
1445
+ }
675
1446
  }
676
1447
  };
677
- // USE EFFECT HOOK THAT TRANSFORMS "FILTERS ARRAY INTO AST"
678
- useEffect(() => {
679
- if (filters.length || dateRange) {
680
- setAST(generateNewAST(true));
681
- setASTNoDateColumn(generateNewAST(false));
1448
+ // Convert an array of columns to a map where the name is the
1449
+ // key and the value is the column node.
1450
+ const columnArrayToMap = (columns) => {
1451
+ const columnMap = {};
1452
+ for (const col of columns) {
1453
+ const key = col.expr?.value ?? col.expr?.column ?? col.as;
1454
+ columnMap[key] = col;
682
1455
  }
683
- }, [filters, dateRange]);
684
- const removeFilter = index => {
685
- setFilters(oldFilters => {
686
- const newFilters = [...oldFilters];
687
- newFilters.splice(index, 1);
688
- return newFilters;
689
- });
690
- setIndexBeingEdited(-1);
691
- };
692
- const computeStats = useCallback(column => {
693
- if ((!computedColumns[column.name] ||
694
- computedColumns[column.name].length === 0) &&
695
- data) {
696
- const basicType = getPostgresBasicType(column);
697
- let result;
698
- if (basicType === 'number') {
699
- let min = Infinity, max = -Infinity;
700
- data.forEach(row => {
701
- const value = row[column.name];
702
- min = Math.min(min, value);
703
- max = Math.max(max, value);
704
- });
705
- result = { min, max };
706
- }
707
- else if (basicType === 'string') {
708
- const freqMap = {};
709
- data.forEach(row => {
710
- const value = row[column.name];
711
- if (value !== null && value !== undefined) {
712
- freqMap[value] = (freqMap[value] || 0) + 1;
1456
+ return columnMap;
1457
+ };
1458
+ const applyFormatting = (response, columns) => {
1459
+ const columnFormatters = {};
1460
+ const columnMap = columnArrayToMap(columns);
1461
+ response.fields.forEach((field) => {
1462
+ // TODO: columnMap[field.name] silently breaks for columnField columns
1463
+ const formatType = getPostgresBasicType(field);
1464
+ if (formatType === 'date') {
1465
+ columnFormatters[field.name] = (x) => {
1466
+ const d = new Date(x);
1467
+ // check if d is a valid date
1468
+ if (isNaN(d.getTime())) {
1469
+ return 'Invalid Date';
713
1470
  }
714
- });
715
- result = Object.entries(freqMap)
716
- .sort((a, b) => b[1] - a[1])
717
- .slice(0, 6)
718
- .map(([key]) => key);
1471
+ d.setMinutes(d.getMinutes() + d.getTimezoneOffset()); // TZ adjust
1472
+ if (columnMap[field.name]?.expr.type === 'function' &&
1473
+ columnMap[field.name]?.expr.name.toLowerCase() === 'date_trunc' &&
1474
+ columnMap[field.name]?.expr.args.value[0].value.toLowerCase() ===
1475
+ 'month') {
1476
+ return d.toLocaleString('default', {
1477
+ month: 'short',
1478
+ year: 'numeric',
1479
+ });
1480
+ }
1481
+ else if (columnMap[field.name]?.expr.type === 'function' &&
1482
+ columnMap[field.name]?.expr.name.toLowerCase() === 'date_trunc' &&
1483
+ columnMap[field.name]?.expr.args.value[0].value.toLowerCase() ===
1484
+ 'quarter') {
1485
+ return `Q${getQuarter(d)} ${d.getFullYear()}`;
1486
+ }
1487
+ else if (columnMap[field.name]?.expr.type === 'function' &&
1488
+ columnMap[field.name]?.expr.name.toLowerCase() === 'date_trunc' &&
1489
+ columnMap[field.name]?.expr.args.value[0].value.toLowerCase() ===
1490
+ 'year') {
1491
+ return d.toLocaleString('default', {
1492
+ year: 'numeric',
1493
+ });
1494
+ }
1495
+ return DATE_FMT.format(d);
1496
+ };
1497
+ }
1498
+ else if (formatType === 'number') {
1499
+ columnFormatters[field.name] = (x) => {
1500
+ if (columnMap[field.name]?.expr.type === 'extract' &&
1501
+ columnMap[field.name]?.expr.args.field.toLowerCase() === 'dow') {
1502
+ return DAY_OF_WEEK[x];
1503
+ }
1504
+ else if (columnMap[field.name]?.expr.type === 'extract' &&
1505
+ columnMap[field.name]?.expr.args.field.toLowerCase() === 'month') {
1506
+ return MONTH_OF_YEAR[x - 1];
1507
+ }
1508
+ else if (`${x}`.includes('.')) {
1509
+ // return MONEY_FMT.format(Number(x ?? 0.0));
1510
+ return Number(x ?? 0.0).toFixed(2);
1511
+ }
1512
+ return x ?? 0.0;
1513
+ };
719
1514
  }
720
1515
  else {
721
- // Handle other column types if necessary
1516
+ columnFormatters[field.name] = (x) => x;
1517
+ }
1518
+ });
1519
+ return response.rows.map((row) => {
1520
+ const newRow = {};
1521
+ Object.keys(row).forEach((key) => (newRow[key] = columnFormatters[key]
1522
+ ? columnFormatters[key](row[key])
1523
+ : row[key]));
1524
+ return newRow;
1525
+ });
1526
+ };
1527
+ // Returns whether a where-clause contains a nested subquery.
1528
+ const isSubquery = (node) => {
1529
+ if (!node)
1530
+ return false;
1531
+ if (node.ast)
1532
+ return true;
1533
+ if (node.left && isSubquery(node.left))
1534
+ return true;
1535
+ if (node.right && isSubquery(node.right))
1536
+ return true;
1537
+ if (node.value && Array.isArray(node.value)) {
1538
+ for (const value of node.value) {
1539
+ if (isSubquery(value))
1540
+ return true;
722
1541
  }
723
- setComputedColumns({
724
- ...computedColumns,
725
- [column.name]: result,
726
- });
727
1542
  }
728
- }, [data, computedColumns]);
729
- // Call this function whenever the selected column changes
730
- useEffect(() => {
731
- computeStats(selectedColumn);
732
- }, [selectedColumn, data]);
733
- // Use the results directly in your component
734
- const columnStats = computedColumns[selectedColumn.name];
735
- // useEffect(() => {
736
- // if (AST && AST.from[0].table) {
737
- // const parser = new Parser();
738
- // const sqlQuery = parser.sqlify(AST, { database: "PostgresQL" });
739
- // if (sqlQuery) {
740
- // runQuery(sqlQuery);
741
- // return;
742
- // }
743
- // }
744
- // }, [AST]);
745
- useEffect(() => {
746
- const getSqlQueryFromAST = async () => {
747
- try {
748
- if (AST && AST.from[0].table) {
749
- const resp = await getDataFromCloud(client, 'sqlify', {
750
- ast: AST,
1543
+ return false;
1544
+ };
1545
+ const handleAsk = async () => {
1546
+ if (!aiPrompt) {
1547
+ return;
1548
+ }
1549
+ try {
1550
+ setLoading(true);
1551
+ let res, data, ast;
1552
+ let numRetries = 0;
1553
+ const MAX_RETRIES = 3;
1554
+ // refetch the request if it comes back and we know it's invalid.
1555
+ // TODO: remove this to allow joins later down the road
1556
+ let isTableJoin = !ast || !ast.from || ast.from.length !== 1;
1557
+ while (isTableJoin || isSubquery(ast?.where)) {
1558
+ if (numRetries === MAX_RETRIES)
1559
+ break;
1560
+ if (!activeQuery || (ast && (isTableJoin || isSubquery(ast?.where)))) {
1561
+ res = await fetch('https://quill-344421.uc.r.appspot.com/magic', {
1562
+ method: 'POST',
1563
+ headers: { 'Content-Type': 'application/json' },
1564
+ body: JSON.stringify({
1565
+ initialQuestion: aiPrompt,
1566
+ publicKey: client.publicKey,
1567
+ }),
1568
+ });
1569
+ }
1570
+ else {
1571
+ res = await fetch('https://quill-344421.uc.r.appspot.com/magic/edit', {
1572
+ method: 'POST',
1573
+ headers: { 'Content-Type': 'application/json' },
1574
+ body: JSON.stringify({
1575
+ sqlQuery: activeQuery,
1576
+ initialQuestion: aiPrompt,
1577
+ publicKey: client.publicKey,
1578
+ }),
751
1579
  });
752
- const newSqlQuery = resp.query; // assuming the response contains the SQL query
753
- if (newSqlQuery && newSqlQuery !== sqlQuery) {
754
- onChangeQuery(newSqlQuery);
755
- setSqlQuery(newSqlQuery);
756
- handleRunQuery(newSqlQuery);
1580
+ }
1581
+ data = await res.json();
1582
+ ast = data?.ast?.length ? data?.ast[0] : data?.ast;
1583
+ // TODO: Debug invalid table joins in handleAsk
1584
+ isTableJoin =
1585
+ ast?.type !== 'bigquery' &&
1586
+ (!ast || !ast.from || ast.from.length !== 1);
1587
+ numRetries += 1;
1588
+ }
1589
+ if (numRetries === MAX_RETRIES) {
1590
+ console.error('[Error]: Max retries exceeded.');
1591
+ console.info(`%c[Prompt]: ${aiPrompt}`, 'color: dimgray');
1592
+ setErrorMessage("Error: Couldn't process your request, please re-word your prompt.");
1593
+ return;
1594
+ }
1595
+ let newAst, groupByPivot;
1596
+ if (ast) {
1597
+ // Unwrap the ast object, supporting many possible types
1598
+ ast = ast.length ? ast[0] : ast;
1599
+ newAst = convertBigQuery(ast);
1600
+ newAst = convertWildcardColumns(newAst, schemaTables); // must go before groupby
1601
+ ({ ast: newAst, pivot: groupByPivot } = convertGroupBy(newAst, pivot, schemaTables));
1602
+ newAst = convertStringComparison(newAst, client.databaseType);
1603
+ newAst = convertRemoveSimpleParentheses(newAst);
1604
+ // newAst = convertDateComparison(newAst); // TODO
1605
+ ast = newAst; // so we fetch data for newAst later.
1606
+ const table = getTableNames(newAst)[0] ?? initialTableName;
1607
+ setPivot(groupByPivot);
1608
+ setSelectedColumns(deepCopy(newAst).columns?.map((column) => {
1609
+ if (column.expr.type === 'column_ref') {
1610
+ return `${table}.${column.expr.column}`;
1611
+ }
1612
+ else if (column.as) {
1613
+ return `${table}.${column.as}`;
757
1614
  }
1615
+ return `${table}.${column.expr.value}`;
1616
+ }));
1617
+ setBaseAst(deepCopy({ ...newAst }));
1618
+ setFormData(deepCopy(newAst.where));
1619
+ setTopLevelBinaryOperator(
1620
+ // @ts-ignore
1621
+ newAst?.where ? newAst?.where?.operator : 'AND');
1622
+ if (groupByPivot)
1623
+ return; // the useEffect will handle the rest
1624
+ }
1625
+ const res2 = await fetch('https://quill-344421.uc.r.appspot.com/patterns', {
1626
+ method: 'POST',
1627
+ headers: {
1628
+ 'Content-Type': 'application/json',
1629
+ },
1630
+ body: JSON.stringify({
1631
+ ast: ast,
1632
+ publicKey: client.publicKey,
1633
+ orgId: '2',
1634
+ }),
1635
+ });
1636
+ const data2 = await res2.json();
1637
+ if (data2.rows && data2.rows.length) {
1638
+ const tables = getTableNames(newAst);
1639
+ const table = tables.length >= 1 ? tables[0] : initialTableName;
1640
+ if (groupByPivot) {
1641
+ const pivotedData = applyPivot(data2, groupByPivot, uniqueValues[table]);
1642
+ console.info(`%c[Pivot]: ${JSON.stringify(groupByPivot)}`, 'color: dimgray');
1643
+ setPivotData(pivotedData);
1644
+ }
1645
+ else {
1646
+ setRows(data2.rows);
1647
+ setFields(data2.fields);
758
1648
  }
759
1649
  }
760
- catch (err) {
761
- console.error(err);
1650
+ else {
1651
+ setRows([]);
1652
+ setFields([]);
1653
+ }
1654
+ if (data2.query) {
1655
+ setActiveQuery(data2.query);
1656
+ }
1657
+ else {
1658
+ setActiveQuery('');
1659
+ }
1660
+ if (data2.errorMessage) {
1661
+ setErrorMessage(`Error: ${data2.errorMessage}`);
762
1662
  }
1663
+ else {
1664
+ setErrorMessage('');
1665
+ }
1666
+ }
1667
+ catch (e) {
1668
+ console.error(e);
1669
+ setErrorMessage(`${e.name}: ${e.message}`);
1670
+ }
1671
+ finally {
1672
+ setLoading(false);
1673
+ setAiPrompt('');
1674
+ }
1675
+ };
1676
+ const handleDeleteColumn = (name) => {
1677
+ if (!baseAst || !baseAst.columns.length || selectedColumns.length === 1) {
1678
+ clearAllState();
1679
+ return;
1680
+ }
1681
+ setSelectedColumns((selectedColumns) => selectedColumns.filter((column) => !column.endsWith(name)));
1682
+ const columns = baseAst.columns.filter((col) => {
1683
+ if (col.expr.type === 'column_ref') {
1684
+ return col.expr.column !== name;
1685
+ }
1686
+ else if (col.as) {
1687
+ return col.as !== name;
1688
+ }
1689
+ return col.expr.value !== name;
1690
+ });
1691
+ if (columns.length === 0) {
1692
+ clearAllState();
1693
+ return;
1694
+ }
1695
+ const newAst = { ...baseAst, columns };
1696
+ setBaseAst(deepCopy(newAst));
1697
+ };
1698
+ function TopLevelBooleanSwitch({ node, keyPrefix, handleOperatorChange, }) {
1699
+ return (_jsx("div", { style: { width: 'fit-content' }, children: _jsx(Tabs, { defaultValue: node.operator, options: DEFAULT_TAB_OPTIONS, onValueChange: (value) => handleOperatorChange(value, node, keyPrefix) }) }));
1700
+ }
1701
+ const DraggableItem = ({ id, label, onDelete }) => {
1702
+ const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: id });
1703
+ const style = {
1704
+ transform: DND_CSS.Transform.toString(transform),
1705
+ transition,
763
1706
  };
764
- getSqlQueryFromAST();
765
- }, [AST]);
766
- useEffect(() => {
767
- const getSqlQueryFromAST = async () => {
768
- try {
769
- if (ASTNoDateColumn && ASTNoDateColumn.from[0].table) {
770
- const resp = await getDataFromCloud(client, 'sqlify', {
771
- ast: ASTNoDateColumn,
772
- });
773
- const newSqlQuery = resp.query; // assuming the response contains the SQL query
774
- if (newSqlQuery && newSqlQuery !== sqlQueryNoDateColumn) {
775
- setSqlQueryNoDateColumn(newSqlQuery);
1707
+ return (_jsx("div", { style: { ...style }, ref: setNodeRef, children: _jsx(DraggableColumn, { label: label, onDelete: onDelete, children: _jsx("div", { style: {
1708
+ cursor: 'grab',
1709
+ }, ...attributes, ...listeners, children: _jsx(HandleButton, {}) }) }) }));
1710
+ };
1711
+ function DraggableColumns() {
1712
+ const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, {
1713
+ coordinateGetter: sortableKeyboardCoordinates,
1714
+ }));
1715
+ // When a drag event ends, switch the item order.
1716
+ function handleDragEnd(event) {
1717
+ const { active, over } = event;
1718
+ if (active.id !== over.id) {
1719
+ const oldIndex = orderedColumnNames.findIndex((c) => c.endsWith(active.id));
1720
+ const newIndex = orderedColumnNames.findIndex((c) => c.endsWith(over.id));
1721
+ const newOrder = arrayMove(orderedColumnNames, oldIndex, newIndex);
1722
+ setOrderedColumnNames(newOrder);
1723
+ const orderedSelectedColumns = [];
1724
+ for (const value of newOrder) {
1725
+ const [_, column] = value.split('.');
1726
+ if (selectedColumns.includes(value)) {
1727
+ orderedSelectedColumns.push(column);
776
1728
  }
777
1729
  }
1730
+ // If there is already an AST saved in state, only update the columns
1731
+ // otherwise fill in the defaultAST shape and also update columns.
1732
+ const fallbackAST = {
1733
+ ...defaultAST,
1734
+ from: [{ ...defaultTable }],
1735
+ columns: orderedSelectedColumns.map((name) => nameToColumn(name)),
1736
+ };
1737
+ const newBaseAst = {
1738
+ ...baseAst,
1739
+ columns: baseAst?.columns.length
1740
+ ? orderedSelectedColumns.map((name) => nameToColumn(name))
1741
+ : baseAst?.columns,
1742
+ };
1743
+ const newAst = baseAst ? newBaseAst : fallbackAST;
1744
+ setBaseAst(newAst);
1745
+ }
1746
+ }
1747
+ const columnNamesInAst = baseAst?.columns.map((col) => {
1748
+ if (col.expr.type === 'column_ref') {
1749
+ return col.expr.column;
778
1750
  }
779
- catch (err) {
780
- console.error(err);
1751
+ else if (col.as) {
1752
+ return col.as;
781
1753
  }
782
- };
783
- getSqlQueryFromAST();
784
- }, [ASTNoDateColumn]);
785
- if (!schema || !schema.length) {
786
- return null;
1754
+ else if (col.expr && col.expr.type === 'aggr_func') {
1755
+ if (col.expr.args) {
1756
+ return `${col.expr.name.toLowerCase()}(${col.expr.args.expr.value})`;
1757
+ }
1758
+ return col.expr.name;
1759
+ }
1760
+ return col.expr.value;
1761
+ }) ?? [];
1762
+ return (_jsx(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragEnd: handleDragEnd, children: _jsx(SortableContext, { items: columnNamesInAst, strategy: verticalListSortingStrategy, children: _jsxs("div", { style: {
1763
+ display: 'flex',
1764
+ flexDirection: 'column',
1765
+ gap: 8,
1766
+ }, children: [columnNamesInAst.map((name) => (_jsx(DraggableItem, { id: name, label: name, onDelete: () => handleDeleteColumn(name) }, name))), columnNamesInAst?.length > 0 && _jsx("div", { style: { height: 6 } })] }) }) }));
787
1767
  }
788
- return (_jsxs("div", { style: { fontFamily: theme?.fontFamily, ...containerStyle }, ref: parentRef, children: [_jsx("div", { style: {
789
- display: 'flex',
790
- // marginLeft: '25px',
791
- // marginRight: '25px',
792
- justifyContent: 'end',
793
- }, children: _jsxs("div", { style: { display: 'flex', flexDirection: 'row', gap: '12px' }, children: [editSQLEnabled && (_jsx(SecondaryButtonComponent, { onClick: () => {
794
- if (navigateToSQLEditor) {
795
- navigateToSQLEditor(sqlQuery);
796
- }
797
- }, label: "Edit Query" })), _jsx(AddFilterModal2, { filters: filters, selectedColumn: selectedColumn, numberStart: numberStart, numberEnd: numberEnd, setDateRange: setFilterDateRange, dateRange: filterDateRange, columnStats: columnStats, stringFilterValues: stringFilterValues, setStringFilterValues: setStringFilterValues, addFilter: addFilter, parentRef: parentRef, setSelectedColumn: setSelectedColumn, setNumberStart: setNumberStart, setNumberEnd: setNumberEnd, selectedTable: selectedTable, columnType: columnType, removeFilter: removeFilter, selectFilter: selectFilter, indexBeingEdited: indexBeingEdited, updateFilter: updateFilter, SelectComponent: SelectComponent, ButtonComponent: ButtonComponent, PopoverComponent: PopoverComponent, TextInputComponent: TextInputComponent, LabelComponent: LabelComponent, tagStyle: tagStyle, theme: theme, setIsOpen: setIsAddFilterModalOpen, isOpen: isAddFilterModalOpen, selectedTagBorderColor: selectedTagBorderColor }), _jsx(PivotModal, { pivotRowField: pivotRowField, setPivotRowField: setPivotRowField, pivotColumnField: pivotColumnField, setPivotColumnField: setPivotColumnField, pivotValueField: pivotValueField, setPivotValueField: setPivotValueField, pivotAggregation: pivotAggregation, setPivotAggregation: setPivotAggregation, createdPivots: createdPivots, setCreatedPivots: setCreatedPivots, recommendedPivots: recommendedPivots, setRecommendedPivots: setRecommendedPivots, selectedTable: selectedTable, SelectComponent: SelectComponent, ButtonComponent: ButtonComponent, PopoverComponent: PopoverComponent, LabelComponent: LabelComponent, HeaderComponent: HeaderComponent, TextComponent: TextComponent, popUpTitle: pivotPopUpTitle, setPopUpTitle: setPivotPopUpTitle, theme: theme, data: data, parentRef: parentRef, columns: columns, setIsOpen: setIsPivotModalOpen, isOpen: isPivotModalOpen, showUpdatePivot: isEdittingPivot, setShowUpdatePivot: setIsEdittingPivot, selectedPivotIndex: selectedPivotIndex, setSelectedPivotIndex: setSelectedPivotIndex, removePivot: removePivot, selectPivot: selectPivot, dateRange: dateRange, rightAlign: true })] }) }), chartBuilderEnabled && columns.length > 0 && (_jsxs("div", { children: [_jsx("div", { style: {
798
- display: 'flex',
799
- flexDirection: 'row',
800
- alignItems: 'center',
801
- justifyContent: 'flex-end',
802
- width: '100%',
803
- height: '70px',
804
- gap: '8px',
805
- }, children: _jsx(ButtonComponent, { onClick: () => setIsChartBuilderOpen(true), label: "Add to dashboard" }) }), _jsx(ChartBuilder, { rows: data, columns: columns, fields: fields, query: sqlQuery, queryNoDateColumn: sqlQueryNoDateColumn, pivot: selectedPivot, title: chartBuilderTitle, buttonLabel: chartBuilderButtonLabel, isOpen: isChartBuilderOpen, setIsOpen: setIsChartBuilderOpen, showTableFormatOptions: showTableFormatOptions, showDateFieldOptions: showDateFieldOptions, showAccessControlOptions: showAccessControlOptions, dateRange: dateRange, recommendedPivots: recommendedPivots, destinationDashboard: destinationDashboard, dateColumn: dateColumn })] }))] }));
806
- }
807
- function FilterTag({ Label, id, label, removeFilter, isSelected, index, setIsOpen, selectFilter, theme, tagStyle, selectedTagBorderColor, }) {
808
- const handleRemoveFilter = () => {
809
- removeFilter(index);
810
- };
811
- const handleSelectFilter = () => {
812
- selectFilter(index);
813
- setIsOpen(true);
814
- };
815
- const styles = tagStyle || {
816
- cursor: 'pointer',
817
- borderRadius: 8,
818
- border: '1px solid',
819
- backgroundColor: '#EFF0FC',
820
- paddingLeft: '12px',
821
- paddingRight: '8px',
822
- height: 30,
823
- display: 'flex',
824
- alignItems: 'center',
825
- fontSize: 13,
826
- fontWeight: 'medium',
827
- color: theme?.primaryTextColor,
828
- fontFamily: theme?.fontFamily,
829
- whiteSpace: 'nowrap',
830
- textOverflow: 'ellipsis',
831
- outline: 'none',
832
- maxWidth: 120,
833
- };
834
- const borderColor = {
835
- borderColor: isSelected
836
- ? selectedTagBorderColor || '#B3B4BD'
837
- : styles.borderColor || '#EFF0FC',
838
- };
839
- return (_jsxs("div", { id: id, onClick: handleSelectFilter, style: { ...styles, ...borderColor }, children: [_jsx("div", { style: {
840
- textOverflow: 'ellipsis',
841
- whiteSpace: 'nowrap',
842
- overflow: 'hidden',
843
- }, children: label }), _jsx("div", {
844
- // onClick={handleRemoveFilter}
845
- onClick: e => {
846
- e.stopPropagation(); // Prevents the event from bubbling up to the parent
847
- handleRemoveFilter();
848
- }, style: {
849
- display: 'flex',
850
- flexDirection: 'row',
851
- alignItems: 'center',
852
- cursor: 'pointer',
853
- paddingLeft: '6px',
854
- }, children: _jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: tagStyle?.color || theme?.primaryTextColor, height: "20", width: "20", children: _jsx("path", { fillRule: "evenodd", d: "M5.47 5.47a.75.75 0 011.06 0L12 10.94l5.47-5.47a.75.75 0 111.06 1.06L13.06 12l5.47 5.47a.75.75 0 11-1.06 1.06L12 13.06l-5.47 5.47a.75.75 0 01-1.06-1.06L10.94 12 5.47 6.53a.75.75 0 010-1.06z", clipRule: "evenodd" }) }) })] }));
855
- }
856
- const AddFilterModal2 = ({ filters, selectedColumn, numberStart, numberEnd, setDateRange, dateRange, columnStats, stringFilterValues, setStringFilterValues, addFilter, setSelectedColumn, setNumberStart, setNumberEnd, selectedTable, columnType, removeFilter, selectFilter, indexBeingEdited, updateFilter, SelectComponent, ButtonComponent, PopoverComponent, LabelComponent, theme, TextInputComponent, tagStyle, selectedTagBorderColor, parentRef, }) => {
857
- const [isOpen, setIsOpen] = useState(false);
858
- return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column' }, children: [_jsxs("div", { style: {
859
- position: 'relative',
860
- display: 'inline-block',
861
- textAlign: 'left',
862
- }, children: [_jsx("div", { style: {
863
- display: 'flex',
864
- flexDirection: 'row',
865
- alignItems: 'center',
866
- }, children: filters.length > 0 && (_jsx("span", { style: {
867
- height: 10,
868
- width: 10,
869
- backgroundColor: theme.primaryButtonColor,
870
- borderRadius: '50%',
871
- position: 'absolute',
872
- top: -2,
873
- right: -2,
874
- } })) }), _jsx(PopoverComponent, { parentRef: parentRef, label: "Add filter", style: { right: 0, minWidth: 400, overflow: 'visible' }, isOpen: isOpen, onClose: () => setIsOpen(false), title: "Add filter", setIsOpen: setIsOpen, children: _jsxs("div", { style: {
875
- backgroundColor: 'rgb(255, 255, 255)',
1768
+ const allNumericColumns = getNumericColumns().map((column) => ({
1769
+ label: column.displayName,
1770
+ value: column.name,
1771
+ }));
1772
+ const allNonNumericColumns = getNonNumericColumns().map((column) => ({
1773
+ label: column.displayName,
1774
+ value: column.name,
1775
+ }));
1776
+ const allStringColumns = getStringColumns().map((column) => ({
1777
+ label: column.displayName,
1778
+ value: column.name,
1779
+ }));
1780
+ if (loading) {
1781
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'row', height: '100%' }, children: [_jsxs(Sidebar, { children: [_jsx(SidebarHeading, { label: "Columns" }), _jsx("div", { style: { height: 4, width: '100%' } }), _jsx(DraggableColumns, {}), _jsx(Popover, { isOpen: openPopover === 'AddColumnPopover', trigger: _jsx(SecondaryButton, { onClick: () => {
1782
+ if (!openPopover) {
1783
+ setOpenPopover('AddColumnPopover');
1784
+ }
1785
+ }, children: "Select columns" }), title: "Select columns", onClose: () => {
1786
+ setIsPending(false);
1787
+ setActiveEditItem(null);
1788
+ setActivePath(null);
1789
+ setOpenPopover(null);
1790
+ }, children: _jsx(AddColumnPopover, { onSave: () => {
1791
+ setActiveEditItem(null);
1792
+ setActivePath(null);
1793
+ setOpenPopover(null);
1794
+ }, orderedColumnNames: orderedColumnNames, setOrderedColumnNames: setOrderedColumnNames, selectedColumns: selectedColumns, setSelectedColumns: setSelectedColumns, isSelectedAllColumns: isSelectedAllColumns, clearAllState: clearAllState, nameToColumn: nameToColumn, baseAst: baseAst, setBaseAst: setBaseAst, pivot: pivot, initialTableName: initialTableName, defaultAST: defaultAST, defaultTable: defaultTable, setPivot: setPivot, TextInput: TextInput, SelectColumn: SelectColumn, SecondaryButton: SecondaryButton, Button: Button, HandleButton: HandleButton }) }), _jsx("div", { style: { height: 28, width: '100%' } }), _jsx(SidebarHeading, { label: "Filters" }), _jsx("div", { style: { height: 4, width: '100%' } }), formData && (_jsx("div", { style: {
876
1795
  display: 'flex',
877
1796
  flexDirection: 'column',
878
- }, children: [filters.length > 0 && (_jsx("div", { style: { paddingBottom: 20 }, children: _jsx("div", { style: {
879
- overflowY: 'scroll',
880
- maxHeight: '300px',
881
- display: 'flex',
882
- gap: '5px',
883
- flexDirection: 'row',
884
- flexWrap: 'wrap',
885
- alignItems: 'center',
886
- }, children: filters.map((elem, index) => (_jsx(FilterTag, { id: "filter-tag", Label: LabelComponent, label: elem.tag, removeFilter: removeFilter, selectFilter: selectFilter, isSelected: index === indexBeingEdited, index: index, theme: theme, setIsOpen: setIsOpen, tagStyle: tagStyle, selectedTagBorderColor: selectedTagBorderColor }, 'filter' + index))) }) })), _jsx(LabelComponent, { children: "Column" }), _jsx(SelectComponent, { label: 'Column', id: "custom-select", value: selectedColumn.name, onChange: e => {
887
- const column = selectedTable.columns.find(c => c.name === e);
888
- setSelectedColumn(column);
889
- }, options: selectedTable.columns
890
- .filter(elem => !(elem.name === 'id' || elem.name.endsWith('_id')))
891
- .map(elem => {
892
- return { label: elem.name, value: elem.name };
893
- }) }), columnType === 'number' && (_jsx("div", { children: _jsxs("div", { style: {
894
- display: 'flex',
895
- flexDirection: 'row',
896
- alignItems: 'center',
897
- justifyContent: 'space-between',
898
- }, children: [_jsxs("div", { style: {
899
- display: 'flex',
900
- flexDirection: 'column',
901
- marginTop: '20px',
902
- }, children: [_jsx(LabelComponent, { children: "Minimum" }), _jsx(TextInputComponent, { id: "min-input", value: numberStart, placeholder: "Minimum", onChange: value => setNumberStart(value) })] }), _jsx("div", { style: { width: 16 } }), _jsxs("div", { style: {
903
- display: 'flex',
904
- flexDirection: 'column',
905
- marginTop: '20px',
906
- }, children: [_jsx(LabelComponent, { children: "Maximum" }), _jsx(TextInputComponent, { id: "max-input", placeholder: "Maximum", value: numberEnd, onChange: value => setNumberEnd(value) })] })] }) })), columnType === 'date' && (_jsx("div", { style: {
1797
+ gap: 8,
1798
+ marginBottom: 12,
1799
+ }, children: renderSentence(formData, formData, '', true) })), _jsxs("div", { style: {
1800
+ display: 'flex',
1801
+ flexDirection: 'column',
1802
+ gap: 2.5,
1803
+ alignItems: 'flex-start',
1804
+ }, children: [_jsx(Popover, { isOpen: openPopover === 'AddFilterPopover', title: 'Add filter', trigger: _jsx(SecondaryButton, { onClick: () => {
1805
+ if (!openPopover) {
1806
+ const value = orderedColumnNames[0];
1807
+ const [_table, column] = value.split('.');
1808
+ const columnType = getColumnTypeByName(column);
1809
+ if (isNumericColumnType(columnType)) {
1810
+ const newSubtree = deepCopy(defaultNumericComparison);
1811
+ newSubtree.left.column = column;
1812
+ setActiveEditItem(newSubtree);
1813
+ }
1814
+ else {
1815
+ const newSubtree = deepCopy(defaultEntry);
1816
+ newSubtree.left.args.value[0].column = column;
1817
+ setActiveEditItem(newSubtree);
1818
+ }
1819
+ setOpenPopover('AddFilterPopover');
1820
+ setActivePath('');
1821
+ setIsPending(true);
1822
+ }
1823
+ }, children: "Add filter" }), onClose: () => {
1824
+ setIsPending(false);
1825
+ setActivePath(null);
1826
+ setOpenPopover(null);
1827
+ clearCheckboxes();
1828
+ setTimeout(() => {
1829
+ setActiveEditItem(null);
1830
+ }, 300);
1831
+ }, children: _jsx(AddFilterPopover, { onSave: () => {
1832
+ if (isNodeEmptyCollection(activeEditItem)) {
1833
+ setIsPending(false);
1834
+ setActivePath(null);
1835
+ setOpenPopover(null);
1836
+ clearCheckboxes();
1837
+ setTimeout(() => {
1838
+ setActiveEditItem(null);
1839
+ }, 300);
1840
+ }
1841
+ else {
1842
+ setIsPending(false);
1843
+ handleInsertion(activeEditItem, 'AND', false);
1844
+ setActivePath(null);
1845
+ setOpenPopover(null);
1846
+ clearCheckboxes();
1847
+ setTimeout(() => {
1848
+ setActiveEditItem(null);
1849
+ }, 300);
1850
+ }
1851
+ }, Button: Button, renderNode: renderNode, activeEditItem: activeEditItem }) }), baseAst?.where &&
1852
+ false && ( // temp removed the AddConditionPopover
1853
+ _jsx(Popover, { isOpen: openPopover === 'AddConditionPopover', trigger: _jsx(SecondaryButton, { onClick: () => {
1854
+ if (!openPopover) {
1855
+ setActiveEditItem(deepCopy(defaultEntry));
1856
+ setOpenPopover('AddConditionPopover');
1857
+ setActivePath('');
1858
+ setIsPending(true);
1859
+ }
1860
+ }, children: "Add condition" }), onClose: () => {
1861
+ setIsPending(false);
1862
+ setTimeout(() => {
1863
+ setActiveEditItem(null);
1864
+ }, 300);
1865
+ setActivePath(null);
1866
+ setOpenPopover(null);
1867
+ clearCheckboxes();
1868
+ }, children: _jsx(AddConditionPopover, { onSave: () => {
1869
+ if (isNodeEmptyCollection(activeEditItem)) {
1870
+ setIsPending(false);
1871
+ setTimeout(() => {
1872
+ setActiveEditItem(null);
1873
+ }, 300);
1874
+ setActivePath(null);
1875
+ setOpenPopover(null);
1876
+ clearCheckboxes();
1877
+ }
1878
+ else {
1879
+ setIsPending(false);
1880
+ handleInsertion(activeEditItem, topLevelBinaryOperator, true);
1881
+ setTimeout(() => {
1882
+ setActiveEditItem(null);
1883
+ }, 300);
1884
+ setActivePath(null);
1885
+ setOpenPopover(null);
1886
+ clearCheckboxes();
1887
+ }
1888
+ } }) }))] }), _jsx("div", { style: { height: 28, width: '100%' } }), _jsx(SidebarHeading, { label: "Pivot" }), _jsx("div", { style: { height: 4, width: '100%' } }), pivot !== null && (_jsxs("div", { style: {
1889
+ display: 'flex',
1890
+ flexDirection: 'column',
1891
+ gap: 12,
1892
+ marginBottom: 12,
1893
+ }, children: [_jsxs("div", { children: [_jsx(SidebarSubHeading, { label: "Aggregation" }), _jsx(Select, { onChange: void null, value: pivot.aggregationType, options: [
1894
+ { label: 'sum', value: 'sum' },
1895
+ { label: 'avg', value: 'avg' },
1896
+ { label: 'min', value: 'min' },
1897
+ { label: 'max', value: 'max' },
1898
+ { label: 'count', value: 'count' },
1899
+ ] })] }), _jsxs("div", { children: [_jsx(SidebarSubHeading, { label: "Value field" }), _jsx(Select, { onChange: void null, value: pivot.valueField, options: [
1900
+ { label: 'Select', value: '' },
1901
+ ...allNumericColumns,
1902
+ ] })] }), _jsxs("div", { children: [_jsx(SidebarSubHeading, { label: "Group rows by" }), _jsx(Select, { onChange: void null, value: pivot.rowField ?? '', options: allNonNumericColumns })] }), _jsxs("div", { children: [_jsx(SidebarSubHeading, { label: "Group columns by" }), _jsx(Select, { onChange: void null, value: pivot.columnField ?? '', options: [
1903
+ { label: 'Select', value: '' },
1904
+ ...allStringColumns,
1905
+ ] })] })] })), _jsx(SecondaryButton, { children: pivot === null ? 'Add pivot' : 'Delete pivot' })] }), _jsxs(Container, { children: [_jsxs("form", { style: {
1906
+ display: 'flex',
1907
+ flexDirection: 'row',
1908
+ gap: 12,
1909
+ padding: 1,
1910
+ }, children: [_jsx(TextInput, { placeholder: "Ask a question...", type: "text", style: { width: '100%', fontSize: 14 }, value: aiPrompt }), _jsx(ButtonLoadingState, {}), baseAst && (_jsx(SecondaryButton, { type: "button", onClick: clearAllState, children: "New report" }))] }), baseAst && (_jsxs(_Fragment, { children: [_jsx(TableLoadingState, {}), _jsxs("div", { style: {
907
1911
  display: 'flex',
908
1912
  flexDirection: 'row',
909
- justifyContent: 'space-between',
910
- marginTop: 20,
911
- }, children: _jsx(QuillDateRangePicker, { dateRange: dateRange ? [dateRange[0], dateRange[1]] : [null, null], label: '', onChangeDateRange: dateRange => {
912
- setDateRange([dateRange[0], dateRange[1], null]);
913
- }, preset: dateRange && dateRange.length === 3 && dateRange[2] !== null
914
- ? dateRange[2]
915
- : '', onChangePreset: preset => {
916
- if (typeof preset === 'string') {
917
- setDateRange(getRangeFromPreset(preset));
918
- return;
919
- }
920
- setDateRange([
921
- preset?.startDate || null,
922
- new Date(),
923
- preset?.value || '',
924
- ]);
925
- }, presetOptions: reportBuilderOptions }) })), columnType === 'string' &&
926
- columnStats &&
927
- columnStats.length > 0 && (_jsx("div", { style: {
928
- flex: 'flex',
929
- flexDirection: 'column',
930
- marginTop: '14px',
931
- overflow: 'hidden',
932
- }, children: columnStats.map(value => (_jsx("div", { style: {
933
- display: 'flex',
934
- flexDirection: 'row',
935
- alignItems: 'center',
936
- }, children: _jsxs("div", { style: {
937
- display: 'flex',
938
- flexDirection: 'row',
939
- alignItems: 'center',
940
- paddingTop: 6,
941
- paddingBottom: 6,
942
- }, children: [_jsx(DivCheckbox, { theme: theme, checked: stringFilterValues.includes(value), onChange: () => {
943
- setStringFilterValues(prev => prev.includes(value)
944
- ? prev.filter(v => v !== value)
945
- : [...prev, value]);
946
- } }), _jsx("div", { style: {
947
- marginLeft: 6,
948
- display: 'block',
949
- overflow: 'hidden',
950
- textOverflow: 'ellipsis',
951
- whiteSpace: 'nowrap',
952
- color: theme?.primaryTextColor,
953
- fontFamily: theme?.fontFamily,
954
- }, children: value })] }, value) }, value))) })), _jsx("div", { style: { height: 20 } }), _jsx("div", { children: _jsx(ButtonComponent, { id: "custom-button", onClick: () => {
955
- if (columnType === 'date' && !dateRange) {
956
- return;
1913
+ gap: '12px',
1914
+ }, children: [_jsx("div", { style: { width: '100%' } }), _jsx(SecondaryButton, { type: "button", onClick: () => copyToClipboard(activeQuery), children: isCopying ? '✅ Copied' : 'Copy SQL' }), _jsx(Button, { children: "Add to dashboard" })] })] }))] }), _jsx("style", { children: `body{margin:0;}` })] }));
1915
+ }
1916
+ return (_jsxs("div", { style: {
1917
+ display: 'flex',
1918
+ flexDirection: 'row',
1919
+ height: '100%',
1920
+ overflowY: 'auto',
1921
+ }, children: [_jsxs(Sidebar, { children: [_jsx(SidebarHeading, { label: "Columns" }), _jsx("div", { style: { height: 4, width: '100%' } }), _jsx(DraggableColumns, {}), _jsx(Popover, { isOpen: openPopover === 'AddColumnPopover', title: "Select columns", trigger: _jsx(SecondaryButton, { onClick: () => {
1922
+ if (!openPopover) {
1923
+ setOpenPopover('AddColumnPopover');
1924
+ }
1925
+ }, children: "Select columns" }), onClose: () => {
1926
+ // delay onClose callback so onClick no-ops
1927
+ setTimeout(() => {
1928
+ setIsPending(false);
1929
+ setActiveEditItem(null);
1930
+ setActivePath(null);
1931
+ setOpenPopover(null);
1932
+ }, 100);
1933
+ }, children: _jsx(AddColumnPopover, { onSave: () => {
1934
+ setActiveEditItem(null);
1935
+ setActivePath(null);
1936
+ setOpenPopover(null);
1937
+ }, orderedColumnNames: orderedColumnNames, setOrderedColumnNames: setOrderedColumnNames, selectedColumns: selectedColumns, setSelectedColumns: setSelectedColumns, isSelectedAllColumns: isSelectedAllColumns, clearAllState: clearAllState, nameToColumn: nameToColumn, baseAst: baseAst, setBaseAst: setBaseAst, pivot: pivot, initialTableName: initialTableName, defaultAST: defaultAST, defaultTable: defaultTable, setPivot: setPivot, TextInput: TextInput, SelectColumn: SelectColumn, SecondaryButton: SecondaryButton, Button: Button, HandleButton: HandleButton }) }), _jsx("div", { style: { height: 28, width: '100%' } }), _jsx(SidebarHeading, { label: "Filters" }), _jsx("div", { style: { height: 4, width: '100%' } }), formData && (_jsx("div", { style: {
1938
+ display: 'flex',
1939
+ flexDirection: 'column',
1940
+ gap: 8,
1941
+ marginBottom: 12,
1942
+ }, children: renderSentence(formData, formData, '', true) })), _jsxs("div", { style: {
1943
+ display: 'flex',
1944
+ flexDirection: 'column',
1945
+ gap: 2.5,
1946
+ alignItems: 'flex-start',
1947
+ }, children: [_jsx(Popover, { title: 'Add filter', isOpen: openPopover === 'AddFilterPopover', trigger: _jsx(SecondaryButton, { onClick: (_e) => {
1948
+ if (!openPopover) {
1949
+ const value = orderedColumnNames[0];
1950
+ const [_table, column] = value.split('.');
1951
+ const columnType = getColumnTypeByName(column);
1952
+ if (isNumericColumnType(columnType)) {
1953
+ const newSubtree = deepCopy(defaultNumericComparison);
1954
+ newSubtree.left.column = column;
1955
+ setActiveEditItem(newSubtree);
957
1956
  }
958
- if (indexBeingEdited > -1) {
959
- updateFilter(indexBeingEdited);
960
- return;
1957
+ else {
1958
+ const newSubtree = deepCopy(defaultEntry);
1959
+ newSubtree.left.args.value[0].column = column;
1960
+ setActiveEditItem(newSubtree);
961
1961
  }
962
- addFilter();
963
- }, label: indexBeingEdited > -1 ? 'Edit filter' : 'Add filter' }) })] }) })] }), _jsx("div", { style: { height: '12px' } })] }));
964
- };
965
- const DivCheckbox = ({ onChange, checked, theme }) => {
966
- const toggleCheckbox = () => {
967
- if (onChange) {
968
- onChange(!checked);
969
- }
970
- };
971
- const style = {
972
- // display: 'inline-block',
973
- width: '18px',
974
- height: '18px',
975
- background: checked ? '#384151' : '#fff',
976
- border: checked ? '1px solid #384151' : '1px solid #E7E7E7',
977
- borderRadius: '4px',
978
- position: 'relative',
979
- cursor: 'pointer',
980
- boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
981
- fontFamily: theme?.fontFamily,
982
- display: 'flex',
983
- flexDirection: 'column',
984
- alignItems: 'center',
985
- justifyContent: 'center',
986
- };
987
- return (_jsx("div", { style: style, onClick: toggleCheckbox, "aria-checked": checked,
988
- // className="shadow-sm"
989
- role: "checkbox", children: checked && (
990
- // <CheckIcon
991
- // style={{ color: theme?.backgroundColor, height: 16, width: 16 }}
992
- // className="text-white"
993
- // aria-hidden="true"
994
- // />
995
- _jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: theme?.backgroundColor, height: "16", width: "16", children: _jsx("path", { fillRule: "evenodd", d: "M19.916 4.626a.75.75 0 01.208 1.04l-9 13.5a.75.75 0 01-1.154.114l-6-6a.75.75 0 011.06-1.06l5.353 5.353 8.493-12.739a.75.75 0 011.04-.208z", clipRule: "evenodd" }) })) }));
996
- };
1962
+ setOpenPopover('AddFilterPopover');
1963
+ setActivePath('');
1964
+ setIsPending(true);
1965
+ }
1966
+ }, children: "Add filter" }), onClose: () => {
1967
+ // delay onClose callback so onClick no-ops
1968
+ setTimeout(() => {
1969
+ setIsPending(false);
1970
+ setActivePath(null);
1971
+ setOpenPopover(null);
1972
+ clearCheckboxes();
1973
+ setActiveEditItem(null);
1974
+ }, 200);
1975
+ }, children: _jsx(AddFilterPopover, { onSave: () => {
1976
+ if (isNodeEmptyCollection(activeEditItem)) {
1977
+ setIsPending(false);
1978
+ setActivePath(null);
1979
+ setOpenPopover(null);
1980
+ clearCheckboxes();
1981
+ setTimeout(() => {
1982
+ setActiveEditItem(null);
1983
+ }, 300);
1984
+ }
1985
+ else {
1986
+ setIsPending(false);
1987
+ handleInsertion(activeEditItem, 'AND', false);
1988
+ setActivePath(null);
1989
+ setOpenPopover(null);
1990
+ clearCheckboxes();
1991
+ setTimeout(() => {
1992
+ setActiveEditItem(null);
1993
+ }, 300);
1994
+ }
1995
+ }, Button: Button, renderNode: renderNode, activeEditItem: activeEditItem }) }), baseAst?.where &&
1996
+ false && ( // temp removed the AddConditionPopover
1997
+ _jsx(Popover, { isOpen: openPopover === 'AddConditionPopover', trigger: _jsx(SecondaryButton, { onClick: () => {
1998
+ if (!openPopover) {
1999
+ setActiveEditItem(deepCopy(defaultEntry));
2000
+ setOpenPopover('AddConditionPopover');
2001
+ setActivePath('');
2002
+ setIsPending(true);
2003
+ }
2004
+ }, children: "Add condition" }), onClose: () => {
2005
+ // delay onClose callback so onClick no-ops
2006
+ setTimeout(() => {
2007
+ setIsPending(false);
2008
+ setActiveEditItem(null);
2009
+ setActivePath(null);
2010
+ setOpenPopover(null);
2011
+ clearCheckboxes();
2012
+ }, 200);
2013
+ }, children: _jsx(AddConditionPopover, { onSave: () => {
2014
+ if (isNodeEmptyCollection(activeEditItem)) {
2015
+ setIsPending(false);
2016
+ setTimeout(() => {
2017
+ setActiveEditItem(null);
2018
+ }, 300);
2019
+ setActivePath(null);
2020
+ setOpenPopover(null);
2021
+ clearCheckboxes();
2022
+ }
2023
+ else {
2024
+ setIsPending(false);
2025
+ handleInsertion(activeEditItem, topLevelBinaryOperator, true);
2026
+ setTimeout(() => {
2027
+ setActiveEditItem(null);
2028
+ }, 300);
2029
+ setActivePath(null);
2030
+ setOpenPopover(null);
2031
+ clearCheckboxes();
2032
+ }
2033
+ } }) }))] }), _jsx("div", { style: { height: 28, width: '100%' } }), _jsx(SidebarHeading, { label: "Pivot" }), _jsx("div", { style: { height: 4, width: '100%' } }), pivot !== null && (_jsxs("div", { style: {
2034
+ display: 'flex',
2035
+ flexDirection: 'column',
2036
+ gap: 12,
2037
+ marginBottom: 12,
2038
+ }, children: [_jsxs("div", { children: [_jsx(SidebarSubHeading, { label: "Aggregation" }), _jsx(Select, { value: pivot.aggregationType, onChange: (value) => {
2039
+ setBaseAst(deepCopy(baseAst)); // trigger refetch
2040
+ setPivot({ ...pivot, aggregationType: value });
2041
+ }, options: [
2042
+ { label: 'sum', value: 'sum' },
2043
+ { label: 'avg', value: 'avg' },
2044
+ { label: 'min', value: 'min' },
2045
+ { label: 'max', value: 'max' },
2046
+ { label: 'count', value: 'count' },
2047
+ ] })] }), _jsxs("div", { children: [_jsx(SidebarSubHeading, { label: "Value field" }), _jsx(Select, { value: pivot.valueField, onChange: (value) => {
2048
+ setBaseAst(deepCopy(baseAst)); // trigger refetch
2049
+ setPivot({ ...pivot, valueField: value });
2050
+ }, options: [{ label: 'Select', value: '' }, ...allNumericColumns] })] }), _jsxs("div", { children: [_jsx(SidebarSubHeading, { label: "Group rows by" }), _jsx(Select, { value: pivot.rowField ?? '', onChange: (value) => {
2051
+ setBaseAst(deepCopy(baseAst)); // trigger refetch
2052
+ setPivot({
2053
+ ...pivot,
2054
+ rowField: value === '' ? undefined : value,
2055
+ });
2056
+ }, options: [
2057
+ { label: 'Select', value: '' },
2058
+ ...allNonNumericColumns,
2059
+ ] })] }), _jsxs("div", { children: [_jsx(SidebarSubHeading, { label: "Group columns by" }), _jsx(Select, { value: pivot.columnField ?? '', onChange: (value) => {
2060
+ setBaseAst(deepCopy(baseAst)); // trigger refetch
2061
+ setPivot({
2062
+ ...pivot,
2063
+ columnField: value === '' ? undefined : value,
2064
+ });
2065
+ }, options: [
2066
+ { label: 'Select', value: '' },
2067
+ ...allStringColumns,
2068
+ ].filter((option) => option.value !== pivot.rowField) })] })] })), _jsx(SecondaryButton, { onClick: () => {
2069
+ if (pivot) {
2070
+ setBaseAst(deepCopy(baseAst));
2071
+ setPivot(null);
2072
+ setPivotData(null);
2073
+ }
2074
+ else {
2075
+ if (allNumericColumns.length === 0) {
2076
+ setErrorMessage('Unable to create pivot: Unsupported Schema.');
2077
+ return;
2078
+ }
2079
+ if (!baseAst) {
2080
+ let ast = deepCopy({
2081
+ ...defaultAST,
2082
+ columns: getAllPossibleColumns()
2083
+ .filter((c) => c.table === initialTableName)
2084
+ .map((c) => {
2085
+ const newColumn = deepCopy(defaultColumn);
2086
+ newColumn.expr.column = c.name;
2087
+ return newColumn;
2088
+ }),
2089
+ from: [{ ...defaultTable, table: initialTableName }],
2090
+ where: null,
2091
+ });
2092
+ ast = convertWildcardColumns(ast, schemaTables);
2093
+ setBaseAst(ast);
2094
+ }
2095
+ else {
2096
+ setBaseAst(deepCopy(baseAst));
2097
+ }
2098
+ setPivot({
2099
+ aggregationType: 'sum',
2100
+ valueField: allNumericColumns[0].value,
2101
+ rowField: allNonNumericColumns[0]?.value,
2102
+ columnField: undefined,
2103
+ });
2104
+ }
2105
+ }, children: pivot === null ? 'Add pivot' : 'Delete pivot' }), _jsx("div", { style: { height: 12, width: '100%' } })] }), _jsxs(Container, { children: [_jsxs("form", { onSubmit: handleAsk, style: {
2106
+ display: 'flex',
2107
+ flexDirection: 'row',
2108
+ gap: 12,
2109
+ padding: 1,
2110
+ }, children: [_jsx(TextInput, { type: "text", value: aiPrompt, style: { width: '100%', fontSize: 14 }, onChange: (e) => setAiPrompt(e.target.value), placeholder: baseAst ? 'Ask a follow-up question...' : 'Ask a question...' }), _jsx(Button, { type: "submit", onClick: handleAsk, children: "Ask AI" }), baseAst && (_jsx(SecondaryButton, { type: "button", onClick: clearAllState, children: "New report" }))] }), baseAst && (_jsxs(_Fragment, { children: [loading && errorMessage.length === 0 ? (_jsx(TableLoadingState, {})) : (_jsx(Table, { rows: applyFormatting({
2111
+ rows: pivotData?.rows || rows,
2112
+ fields: pivotData?.fields || fields,
2113
+ }, baseAst?.columns ?? []), columns: enforceOrderOnColumns(Object.keys((pivotData?.rows[0] || rows[0]) ?? {})), error: errorMessage, rowsPerPage: 20 })), _jsxs("div", { style: {
2114
+ display: 'flex',
2115
+ flexDirection: 'row',
2116
+ gap: '12px',
2117
+ }, children: [errorMessage && (_jsx("div", { style: {
2118
+ color: 'red',
2119
+ fontSize: 14,
2120
+ padding: '12px',
2121
+ whiteSpace: 'nowrap',
2122
+ }, children: errorMessage })), _jsx("div", { style: { width: '100%' } }), _jsx(SecondaryButton, { type: "button", onClick: () => copyToClipboard(activeQuery), children: isCopying ? '✅ Copied' : 'Copy SQL' }), _jsx(Button, { onClick: () => {
2123
+ setIsChartBuilderOpen(true);
2124
+ }, children: "Add to dashboard" })] })] }))] }), _jsx("style", { children: `body{margin:0;}` }), _jsx(ChartBuilder, { rows: applyFormatting({ rows, fields }, baseAst?.columns ?? []), columns: processColumnsForChartBuilder(Object.keys(rows[0] ?? {})), fields: fields, pivot: pivot, query: activeQuery, showTableFormatOptions: true, showDateFieldOptions: true, showAccessControlOptions: true, title: "Add to dashboard", isEditMode: false, isOpen: isChartBuilderOpen, setIsOpen: setIsChartBuilderOpen, onAddToDashboardComplete: onAddToDashboardComplete, destinationDashboard: destinationDashboard, dashboardItem: dashboardItem, organizationName: organizationName })] }));
2125
+ }