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