diff --git a/.vscode/settings.json b/.vscode/settings.json index e37420baf7..90a9b54149 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,8 @@ "files.exclude": { "**/*.rollup.cache": true, "**/dist": true, - "**/target": true - } + "**/target": true, + }, + "editor.folding": true, + "editor.foldingStrategy": "indentation", } diff --git a/packages/client/src/components/cell-defaults/code-cell/config.ts b/packages/client/src/components/cell-defaults/code-cell/config.ts index 1e08f3c85e..3d12d9cbb7 100644 --- a/packages/client/src/components/cell-defaults/code-cell/config.ts +++ b/packages/client/src/components/cell-defaults/code-cell/config.ts @@ -10,6 +10,7 @@ export const CodeCellConfig: CellConfig = { }, view: CodeCell, toPixel: ({ type, code }) => { + console.log({ type, code }); code = typeof code === 'string' ? code : code.join('\n'); if (type === 'r') { return `R("${code}");`; diff --git a/packages/client/src/components/cell-defaults/data-import-cell/DataImportCell.tsx b/packages/client/src/components/cell-defaults/data-import-cell/DataImportCell.tsx new file mode 100644 index 0000000000..98fe41e1d5 --- /dev/null +++ b/packages/client/src/components/cell-defaults/data-import-cell/DataImportCell.tsx @@ -0,0 +1,629 @@ +import { observer } from 'mobx-react-lite'; +import { useEffect, useRef, useState } from 'react'; +import { StyledSelect, StyledSelectItem } from '../shared'; +import Editor, { Monaco } from '@monaco-editor/react'; +import { + styled, + Button, + TextField, + InputAdornment, + Typography, + IconButton, + Tooltip, + Stack, + Modal, +} from '@semoss/ui'; + +import { + CropFree, + KeyboardArrowDown, + DriveFileRenameOutlineRounded, + CalendarViewMonth, + JoinInner, + JoinRight, + JoinLeft, + JoinFull, + Edit, +} from '@mui/icons-material'; +import { ActionMessages, CellComponent, CellDef } from '@/stores'; +import { DataImportFormModal } from '../../notebook/DataImportFormModal'; +import { useBlocks, usePixel } from '@/hooks'; +import { editor } from 'monaco-editor'; + +const EDITOR_LINE_HEIGHT = 19; +const EDITOR_MAX_HEIGHT = 500; // ~25 lines + +const FRAME_TYPES = { + PIXEL: { + display: 'Pixel', + value: 'PIXEL', + }, + NATIVE: { + display: 'GRID', + value: 'NATIVE', + }, + PY: { + display: 'Python', + value: 'PY', + }, + R: { + display: 'R', + value: 'R', + }, + GRID: { + display: 'Grid', + value: 'GRID', + }, +}; + +const JOIN_ICONS = { + inner: , + 'right.outer': , + 'left.outer': , + outer: , +}; + +const StyledIconButton = styled(IconButton)(({ theme }) => ({ + marginRight: '7.5px', + marginLeft: '7.5px', +})); + +const StyledPaddedFlexDiv = styled('div')(({ theme }) => ({ + alignItems: 'center', + display: 'flex', +})); + +const StyledFlexDiv = styled('div')(({ theme }) => ({ + alignItems: 'center', + paddingBottom: '0', + marginBottom: '0', + display: 'flex', +})); + +const StyledCalendarViewMonth = styled(CalendarViewMonth)(({ theme }) => ({ + strokeWidth: 0.025, + marginLeft: '-3px', + marginRight: '7px', + color: '#95909C', +})); + +const BlueStyledJoinDiv = styled('div')(({ theme }) => ({ + backgroundColor: theme.palette.primary.selected, + padding: '0px 12px', + borderRadius: '12px', + fontSize: '12.5px', + border: 'none', + color: 'black', + cursor: 'default', + fontWeight: '500', +})); + +const GreenStyledJoinDiv = styled('div')(({ theme }) => ({ + border: 'none', + padding: '0px 12px', + backgroundColor: '#DEF4F3', + borderRadius: '12px', + fontSize: '12.5px', + color: 'black', + cursor: 'default', + fontWeight: '500', +})); + +const StyledJoinTypography = styled(Typography)(({ theme }) => ({ + color: theme.palette.secondary.dark, + marginRight: '12.5px', + marginLeft: '12.5px', + cursor: 'default', +})); + +const StyledModalTitleWrapper = styled(Modal.Title)(({ theme }) => ({ + alignContent: 'center', + display: 'flex', + padding: '0px', +})); + +const StyledTableTitleBubble = styled('div')(({ theme }) => ({ + marginTop: '0px', + marginRight: '15px', + width: 'fit-content', + backgroundColor: '#F1E9FB', + padding: '7.5px 17.5px', + display: 'inline-flex', + borderRadius: '10px', + alignItems: 'center', + fontWeight: '400', + fontSize: '12.5px', + cursor: 'default', +})); + +const StyledContent = styled('div')(({ theme }) => ({ + position: 'relative', + width: '100%', +})); + +const StyledTextField = styled(TextField)(({ theme }) => ({ + '& .MuiInputBase-root': { + color: theme.palette.text.secondary, + gap: theme.spacing(1), + display: 'flex', + height: '30px', + width: '200px', + }, +})); + +const StyledBlockStack = styled(Stack)(({ theme }) => ({ + display: 'block', +})); + +interface JoinObject { + id: string; + joinType: string; + leftTable: string; + rightTable: string; + rightKey: string; + leftKey: string; +} + +// TODO add filters and summaries +// interface FilterObject { +// // structure for filters +// } +// interface summaryObject { +// // structure for summaries +// } + +export interface DataImportCellDef extends CellDef<'data-import'> { + widget: 'data-import'; + parameters: { + databaseId: string; + frameType: 'NATIVE' | 'PY' | 'R' | 'GRID' | 'PIXEL'; + frameVariableName: string; + selectQuery: string; + rootTable: string; + selectedColumns: string[]; + columnAliases: string[]; + tableNames: string[]; + joins: JoinObject[]; + + // TODO add filters and summaries + // filters: FilterObject[]; + // summaries: FilterObject[]; + }; +} + +export const DataImportCell: CellComponent = observer( + (props) => { + const editorRef = useRef(null); + const [showStyledView, setShowStyledView] = useState(true); + const { cell, isExpanded } = props; + const { state } = useBlocks(); + + const [isDataImportModalOpen, setIsDataImportModalOpen] = + useState(false); + + const [cfgLibraryDatabases, setCfgLibraryDatabases] = useState({ + loading: true, + display: {}, + ids: [], + }); + + const myDbs = usePixel<{ app_id: string; app_name: string }[]>( + `MyEngines(engineTypes=['DATABASE']);`, + ); + + useEffect(() => { + if (myDbs.status !== 'SUCCESS') { + return; + } + + const dbIds: string[] = []; + const dbDisplay = {}; + + myDbs.data.forEach((db) => { + dbIds.push(db.app_id); + dbDisplay[db.app_id] = db.app_name; + }); + + setCfgLibraryDatabases({ + loading: false, + display: dbDisplay, + ids: dbIds, + }); + + if (!cell.parameters.databaseId && dbIds.length) { + state.dispatch({ + message: ActionMessages.UPDATE_CELL, + payload: { + path: 'parameters.databaseId', + queryId: cell.query.id, + cellId: cell.id, + value: dbIds[0], + }, + }); + } + }, [myDbs.status, myDbs.data]); + + /** + * Handle mounting of the editor + * @param editor - editor that mounted + * @param monaco - monaco instance + */ + const handleEditorMount = ( + editor: editor.IStandaloneCodeEditor, + monaco: Monaco, + ) => { + editorRef.current = editor; + + // add on change + let ignoreResize = false; + editor.onDidContentSizeChange(() => { + try { + // set the ignoreResize flag + if (ignoreResize) { + return; + } + ignoreResize = true; + + resizeEditor(); + } finally { + ignoreResize = false; + } + }); + + // update the action + editor.addAction({ + id: 'run', + label: 'Run', + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter], + run: (editor) => { + const newValue = editor.getValue(); + + // update with the new code + state.dispatch({ + message: ActionMessages.UPDATE_CELL, + payload: { + path: 'parameters.selectQuery', + queryId: cell.query.id, + cellId: cell.id, + value: newValue, + }, + }); + + state.dispatch({ + message: ActionMessages.RUN_CELL, + payload: { + queryId: cell.query.id, + cellId: cell.id, + }, + }); + }, + }); + + monaco.editor.defineTheme('custom-theme', { + base: 'vs', + inherit: false, + rules: [], + colors: { + 'editor.background': '#FAFAFA', + }, + }); + + monaco.editor.setTheme('custom-theme'); + + // resize the editor + resizeEditor(); + }; + + /** + * Resize the editor + */ + const resizeEditor = () => { + // set the initial height + let height = 0; + + // if expanded scale to lines, but do not go over the max height + if (isExpanded) { + height = Math.min( + editorRef.current.getContentHeight(), + EDITOR_MAX_HEIGHT, + ); + } + + // add the trailing line + height += EDITOR_LINE_HEIGHT; + + editorRef.current.layout({ + width: editorRef.current.getContainerDomNode().clientWidth, + height: height, + }); + }; + + /** + * Handle changes in the editor - currently not in use, will need work if edits are enabled + * @param newValue - newValue + * @returns + */ + const handleEditorChange = (newValue: string) => { + if (cell.isLoading) { + return; + } + + state.dispatch({ + message: ActionMessages.UPDATE_CELL, + payload: { + value: newValue, + path: 'parameters.selectQuery', + queryId: cell.query.id, + cellId: cell.id, + }, + }); + }; + + const openEditModal = () => { + setIsDataImportModalOpen(true); + }; + + return ( + + + {isExpanded && ( + + + { + const value = e.target.value; + state.dispatch({ + message: ActionMessages.UPDATE_CELL, + payload: { + path: 'parameters.databaseId', + queryId: cell.query.id, + cellId: cell.id, + value: value, + }, + }); + }} + > + {Array.from( + cfgLibraryDatabases.ids, + (databaseId, i) => ( + + {cfgLibraryDatabases.display[ + databaseId + ] ?? ''} + + ), + )} + + + + + )} + {showStyledView ? ( + <> + + {cell.parameters.tableNames && + cell.parameters.tableNames.map( + (tableName, tableIdx) => ( + + + + {tableName} + + + ), + )} + + + {isExpanded && + cell.parameters.joins && + cell.parameters.joins.map((join, joinIdx) => ( + + + + + + {join.leftTable} + + + + + + { + JOIN_ICONS[ + join.joinType + ] + } + + + + + + {join.rightTable} + + + + + ON + + + + + {join.leftKey} + + + + + = + + + + + {join.rightKey} + + + + + + ))} + + ) : ( +
+ +
+ )} + {isExpanded && ( + + + + + + + ), + }} + onChange={(e) => { + const value = e.target.value; + state.dispatch({ + message: ActionMessages.UPDATE_CELL, + payload: { + path: 'parameters.frameType', + queryId: cell.query.id, + cellId: cell.id, + value: value, + }, + }); + }} + > + {Object.values(FRAME_TYPES).map((frame, i) => ( + + {frame.display} + + ))} + + + ), + }} + onChange={(e) => { + state.dispatch({ + message: ActionMessages.UPDATE_CELL, + payload: { + path: 'parameters.frameVariableName', + queryId: cell.query.id, + value: e.target.value, + cellId: cell.id, + }, + }); + }} + /> + + )} +
+ {isDataImportModalOpen && ( + + )} +
+ ); + }, +); diff --git a/packages/client/src/components/cell-defaults/data-import-cell/DatabaseTables.tsx b/packages/client/src/components/cell-defaults/data-import-cell/DatabaseTables.tsx new file mode 100644 index 0000000000..149c14d716 --- /dev/null +++ b/packages/client/src/components/cell-defaults/data-import-cell/DatabaseTables.tsx @@ -0,0 +1,143 @@ +import { useState, useMemo } from 'react'; +import { usePixel } from '@/hooks'; +import { + styled, + List, + Stack, + Card, + Divider, + LinearProgress, + Typography, + Collapse, +} from '@semoss/ui'; +import { + AccessTime, + DateRange, + FontDownload, + KeyboardArrowRight, + Numbers, + TableChartOutlined, +} from '@mui/icons-material'; + +const StyledHeader = styled(Stack)(() => ({ + cursor: 'pointer', +})); +const StyledTableScroll = styled(Stack)(({ theme }) => ({ + width: '100%', + overflow: 'auto', + padding: theme.spacing(0.5), +})); +const StyledCard = styled(Card)(({ theme }) => ({ + minWidth: theme.spacing(35), +})); +const StyledList = styled(List)(({ theme }) => ({ + maxHeight: theme.spacing(15), + overflow: 'auto', +})); + +export const DatabaseTables = (props: { databaseId: string }) => { + const [tables, setTables] = useState({}); + const [isLoading, setIsLoading] = useState(true); + + const databaseMetamodel = usePixel<{ + dataTypes: Record; + nodes: { propSet: string[]; conceptualName: string }[]; + }>( + `GetDatabaseMetamodel( database=["${props.databaseId}"], options=["dataTypes"]); `, + ); + + useMemo(() => { + if (databaseMetamodel.status !== 'SUCCESS') { + setIsLoading(true); + return; + } + const { nodes = [], dataTypes = {} } = databaseMetamodel.data; + const retrievedTables = {}; + nodes.forEach((n) => { + const tableName = n.conceptualName; + const filteredDataTypes = Object.keys(dataTypes).filter((colName) => + colName.includes(`${tableName}__`), + ); + retrievedTables[n.conceptualName] = { + columnNames: [...n.propSet], + columnTypes: filteredDataTypes.reduce((acc, colName) => { + acc[colName] = dataTypes[colName]; + return acc; + }, {}), + }; + }); + setTables(retrievedTables); + setIsLoading(false); + }, [databaseMetamodel.status, databaseMetamodel.data]); + + const getIconForDataType = (dataType: string) => { + switch (dataType) { + case 'INT': + case 'DOUBLE': + case 'DECIMAL': + case 'NUMBER': + return ; + case 'STRING': + case 'TEXT': + return ; + case 'DATE': + case 'DATETIME': + return ; + case 'TIME': + return ; + default: + return <>; + } + }; + + if (isLoading) { + return ; + } + + return ( +
+ {isLoading && } + + + {Array.from(Object.keys(tables), (tableName, index) => { + return ( + + + + + + + + + + {Array.from( + tables[tableName].columnNames, + (columnName, index) => { + return ( + + + {getIconForDataType( + tables[tableName] + .columnTypes[ + `${tableName}__${columnName}` + ], + )} + + + + ); + }, + )} + + + ); + })} + + +
+ ); +}; diff --git a/packages/client/src/components/cell-defaults/data-import-cell/config.ts b/packages/client/src/components/cell-defaults/data-import-cell/config.ts new file mode 100644 index 0000000000..9fcd105360 --- /dev/null +++ b/packages/client/src/components/cell-defaults/data-import-cell/config.ts @@ -0,0 +1,28 @@ +import { CellConfig } from '@/stores'; +import { DataImportCell, DataImportCellDef } from './DataImportCell'; + +export const DataImportCellConfig: CellConfig = { + name: 'Data Import', + widget: 'data-import', + view: DataImportCell, + parameters: { + frameVariableName: '', + selectQuery: '', + databaseId: '', + frameType: 'PY', + rootTable: '', + selectedColumns: [], + columnAliases: [], + tableNames: [], + joins: [], + // TODO add filters and summaries + // filters: [], + // summaries: [], + }, + toPixel: ({ frameType, frameVariableName, selectQuery }) => { + return ( + selectQuery.slice(0, -1) + + ` | Import ( frame = [ CreateFrame ( frameType = [ ${frameType} ] , override = [ true ] ) .as ( [ \"${frameVariableName}\" ] ) ] ) ; ` + ); + }, +}; diff --git a/packages/client/src/components/cell-defaults/data-import-cell/index.ts b/packages/client/src/components/cell-defaults/data-import-cell/index.ts new file mode 100644 index 0000000000..82983d57d9 --- /dev/null +++ b/packages/client/src/components/cell-defaults/data-import-cell/index.ts @@ -0,0 +1,2 @@ +export * from './config'; +export * from './DataImportCell'; diff --git a/packages/client/src/components/cell-defaults/index.ts b/packages/client/src/components/cell-defaults/index.ts index 36f89b0ce4..ec8646954a 100644 --- a/packages/client/src/components/cell-defaults/index.ts +++ b/packages/client/src/components/cell-defaults/index.ts @@ -2,6 +2,7 @@ import { CellRegistry } from '@/stores'; import { CodeCellConfig, CodeCellDef } from './code-cell'; import { QueryImportCellConfig, QueryImportCellDef } from './query-import-cell'; +import { DataImportCellConfig, DataImportCellDef } from './data-import-cell'; import { UppercaseTransformationCellConfig, UppercaseTransformationCellDef, @@ -46,6 +47,7 @@ import { export type DefaultCellDefinitions = | CodeCellDef | QueryImportCellDef + | DataImportCellDef | UppercaseTransformationCellDef | UpdateRowTransformationCellDef | ColumnTypeTransformationCellDef @@ -59,6 +61,7 @@ export type DefaultCellDefinitions = export const DefaultCells: CellRegistry = { [CodeCellConfig.widget]: CodeCellConfig, [QueryImportCellConfig.widget]: QueryImportCellConfig, + [DataImportCellConfig.widget]: DataImportCellConfig, [UppercaseTransformationCellConfig.widget]: UppercaseTransformationCellConfig, [UpdateRowTransformationCellConfig.widget]: diff --git a/packages/client/src/components/notebook/DataImportFormModal.tsx b/packages/client/src/components/notebook/DataImportFormModal.tsx new file mode 100644 index 0000000000..9f3c15073a --- /dev/null +++ b/packages/client/src/components/notebook/DataImportFormModal.tsx @@ -0,0 +1,1863 @@ +import { useState, useEffect, useRef } from 'react'; +import { observer } from 'mobx-react-lite'; +import { + Checkbox, + useNotification, + Typography, + TextField, + IconButton, + MenuProps, + Tooltip, + Button, + Menu, + Stack, + Select, + Modal, + Table, + styled, +} from '@semoss/ui'; +import { useBlocks, usePixel, useRootStore } from '@/hooks'; +import { + ActionMessages, + CellStateConfig, + NewCellAction, + QueryState, +} from '@/stores'; +import { + ControlPointDuplicateRounded, + CalendarViewMonth, + KeyboardArrowDown, + FilterListRounded, + AddBox, + JoinInner, + JoinRight, + JoinLeft, + JoinFull, + Warning, +} from '@mui/icons-material'; +import { DefaultCells } from '@/components/cell-defaults'; +import { DataImportCellConfig } from '../cell-defaults/data-import-cell'; +import { useFieldArray, useForm, Controller } from 'react-hook-form'; +import { CodeCellConfig } from '../cell-defaults/code-cell'; +import { TableContainer } from '@mui/material'; +import { LoadingScreen } from '@/components/ui'; + +const StyledDivSecondaryKeyLabel = styled('div')(() => ({ + backgroundColor: '#EBEBEB', + padding: '3px, 4px, 3px, 4px', + width: '37px', + height: '24px', + borderRadius: '3px', + display: 'inline-block', + marginLeft: '7px', + paddingTop: '3px', + textAlign: 'center', +})); + +const StyledDivPrimaryKeyLabel = styled('div')(() => ({ + backgroundColor: '#F1E9FB', + padding: '3px, 4px, 3px, 4px', + width: '37px', + height: '24px', + borderRadius: '3px', + display: 'inline-block', + marginLeft: '7px', + paddingTop: '3px', + textAlign: 'center', +})); + +const StyledDivFitContent = styled('div')(() => ({ + width: 'fit-content', + blockSize: 'fit-content', + display: 'flex', +})); + +const StyledIconButtonMargins = styled(IconButton)(() => ({ + marginLeft: '7.5px', + marginRight: '7.5px', +})); + +const StyledSelectMinWidth = styled(Select)(() => ({ + minWidth: '220px', +})); + +const StyledButtonEditColumns = styled(Button)(() => ({ + marginRight: '15px', +})); + +const StyledDivCenterFlex = styled('div')(() => ({ + alignItems: 'center', + display: 'flex', +})); + +const StyledTypographyMarginRight = styled(Typography)(() => ({ + marginBottom: '-1.5px', + marginRight: '15px', +})); + +const StyledModalActionsUnpadded = styled(Modal.Actions)(() => ({ + display: 'flex', + justifyContent: 'flex-end', + padding: '0px', +})); + +const StyledMarginModalActions = styled(Modal.Actions)(() => ({ + display: 'flex', + justifyContent: 'flex-start', + marginBottom: '15px', + marginTop: '15px', + padding: '0px', +})); + +const StyledPaddedStack = styled(Stack)(() => ({ + backgroundColor: '#FAFAFA', + padding: '16px 16px 16px 16px', + marginBottom: '15px', +})); + +const StyledModalTitle = styled(Typography)(() => ({ + alignContent: 'center', + marginRight: '15px', +})); + +const StyledModalTitleWrapper = styled(Modal.Title)(() => ({ + justifyContent: 'space-between', + alignContent: 'center', + display: 'flex', + padding: '0px', + marginBottom: '15px', + marginTop: '25px', +})); + +const StyledModalTitleUnpaddedWrapper = styled(Modal.Title)(() => ({ + justifyContent: 'space-between', + alignContent: 'center', + display: 'flex', + padding: '0px', +})); + +const ScrollTableSetContainer = styled(TableContainer)(() => ({ + maxHeight: '350px', + overflowY: 'scroll', +})); + +const StyledTableSetWrapper = styled('div')(() => ({ + backgroundColor: '#fff', + marginBottom: '20px', +})); + +const StyledTableTitle = styled(Typography)(() => ({ + marginTop: '15px', + marginLeft: '15px', + marginBottom: '20px', +})); + +const FlexWrapper = styled('div')(() => ({ + marginTop: '15px', + display: 'flex', + padding: '0px', +})); + +const FlexTableCell = styled('div')(() => ({ + alignItems: 'center', + display: 'flex', +})); + +const StyledTableTitleBlueBubble = styled(Typography)(({ theme }) => ({ + backgroundColor: theme.palette.primary.selected, + padding: '7.5px 17.5px', + borderRadius: '10px', + width: 'fit-content', + display: 'flex', + marginTop: '0px', + marginLeft: '0px', + marginBottom: '15px', + alignItems: 'center', +})); + +const SingleTableWrapper = styled('div')(() => ({ + marginRight: '12.5px', + marginBottom: '60px', + marginLeft: '12.5px', +})); + +const CheckAllIconButton = styled(IconButton)(() => ({ + marginLeft: '-10px', +})); + +const AliasWarningIcon = styled(Tooltip)(() => ({ + marginLeft: '10px', + color: 'goldenrod', +})); + +const TableIconButton = styled(Tooltip)(({ theme }) => ({ + color: theme.palette.primary.main, + marginLeft: '-3px', + marginRight: '7px', +})); + +const ColumnNameText = styled(Typography)(() => ({ + fontWeight: 'bold', +})); + +const StyledMenu = styled((props: MenuProps) => ( + +))(({ theme }) => ({ + '& .MuiPaper-root': { + marginTop: theme.spacing(1), + }, + '.MuiList-root': { + padding: 0, + }, +})); + +const StyledMenuItem = styled(Menu.Item)(() => ({ + textTransform: 'capitalize', +})); + +const StyledJoinDiv = styled('div')(({ theme }) => ({ + borderRadius: '12px', + padding: '4px 12px', + fontSize: '14px', + color: 'black', + border: 'none', + cursor: 'default', + backgroundColor: theme.palette.primary.selected, +})); + +const StyledJoinTypography = styled(Typography)(({ theme }) => ({ + cursor: 'default', + marginLeft: '12.5px', + marginRight: '12.5px', + color: theme.palette.secondary.dark, +})); + +type JoinElement = { + leftTable: string; + rightTable: string; + joinType: string; + leftKey: string; + rightKey: string; +}; + +interface Column { + id: number; + tableName: string; + columnName: string; + columnType: string; + userAlias: string; + checked: boolean; +} + +interface Table { + id: number; + columns: Column[]; + name: string; +} + +interface NewFormData { + databaseSelect: string; + tables: Table[]; +} + +type FormValues = { + databaseSelect: string; + joins: JoinElement[]; + tables: Table[]; +}; + +const IMPORT_MODAL_WIDTHS = { + small: '600px', + medium: '1150px', + large: '1150px', +}; + +const SQL_COLUMN_TYPES = ['DATE', 'NUMBER', 'STRING', 'TIMESTAMP']; + +const JOIN_ICONS = { + inner: , + 'right.outer': , + 'left.outer': , + outer: , +}; + +export const DataImportFormModal = observer( + (props: { + query?: QueryState; + previousCellId?: string; + setIsDataImportModalOpen?; + editMode?: boolean; + cell?; + }): JSX.Element => { + const { + query, + previousCellId, + setIsDataImportModalOpen, + editMode, + cell, + } = props; + + const [anchorEl, setAnchorEl] = useState(null); + const [joinTypeSelectIndex, setJoinTypeSelectIndex] = useState(-1); + const { state, notebook } = useBlocks(); + + const { + control: formControl, + setValue: formSetValue, + reset: formReset, + handleSubmit: formHandleSubmit, + watch: dataImportwatch, + } = useForm(); + + const watchedTables = dataImportwatch('tables'); + const watchedJoins = dataImportwatch('joins'); + const [userDatabases, setUserDatabases] = useState(null); + const [importModalPixelWidth, setImportModalPixelWidth] = + useState(IMPORT_MODAL_WIDTHS.small); + const [databaseTableHeaders, setDatabaseTableHeaders] = useState([]); + const [selectedDatabaseId, setSelectedDatabaseId] = useState( + cell ? cell.parameters.databaseId : null, + ); + const getDatabases = usePixel('META | GetDatabaseList ( ) ;'); // making repeat network calls, move to load data modal open + const [databaseTableRows, setDatabaseTableRows] = useState([]); + const [tableNames, setTableNames] = useState([]); + const [isDatabaseLoading, setIsDatabaseLoading] = + useState(false); + const [showPreview, setShowTablePreview] = useState(false); + const [showEditColumns, setShowEditColumns] = useState(true); + const [tableEdgesObject, setTableEdgesObject] = useState(null); + const [aliasesCountObj, setAliasesCountObj] = useState({}); + const { monolithStore } = useRootStore(); + const aliasesCountObjRef = useRef({}); + const [tableEdges, setTableEdges] = useState({}); // + const [rootTable, setRootTable] = useState( + cell ? cell.parameters.rootTable : null, + ); + + const [checkedColumnsCount, setCheckedColumnsCount] = useState(0); + const [selectedTableNames, setSelectedTableNames] = useState(new Set()); + const [shownTables, setShownTables] = useState(new Set()); + const [joinsSet, setJoinsSet] = useState(new Set()); + const pixelStringRef = useRef(''); + const pixelPartialRef = useRef(''); + const [isInitLoadComplete, setIsInitLoadComplete] = useState(false); + const [isJoinSelectOpen, setIsJoinSelectOpen] = useState(false); + const [initEditPrepopulateComplete, setInitEditPrepopulateComplete] = + useState(editMode ? false : true); + + const { fields: newTableFields } = useFieldArray({ + control: formControl, + name: 'tables', + }); + + const { + fields: joinElements, + append: appendJoinElement, + remove: removeJoinElement, + } = useFieldArray({ + control: formControl, + name: 'joins', + }); + + const notification = useNotification(); + + useEffect(() => { + if (editMode) + retrieveDatabaseTablesAndEdges(cell.parameters.databaseId); + }, []); + + useEffect(() => { + setShowTablePreview(false); + setShowEditColumns(true); + }, [selectedDatabaseId]); + + useEffect(() => { + if ( + editMode && + checkedColumnsCount == 0 && + cell.parameters.databaseId == selectedDatabaseId && + newTableFields.length && + !initEditPrepopulateComplete + ) { + prepoulateFormForEdit(cell); + } + }, [newTableFields]); + + useEffect(() => { + if (getDatabases.status !== 'SUCCESS') { + return; + } + setUserDatabases(getDatabases.data); + }, [getDatabases.status, getDatabases.data]); + + useEffect(() => { + if (!editMode || initEditPrepopulateComplete) { + setJoinsStackHandler(); + updateSelectedTables(); + } + }, [checkedColumnsCount]); + + useEffect(() => { + if (showPreview) { + retrievePreviewData(); + } + }, [ + aliasesCountObj, + checkedColumnsCount, + showPreview, + selectedDatabaseId, + ]); + + const getSelectedColumnNames = () => { + const pixelTables = new Set(); + const pixelColumnNames = []; + + watchedTables.forEach((tableObject) => { + const currTableColumns = tableObject.columns; + + currTableColumns.forEach((columnObject) => { + if (columnObject.checked) { + pixelTables.add(columnObject.tableName); + pixelColumnNames.push( + `${columnObject.tableName}__${columnObject.columnName}`, + ); + } + }); + }); + + return pixelColumnNames; + }; + + const getColumnAliases = () => { + const pixelTables = new Set(); + const pixelColumnAliases = []; + + watchedTables.forEach((tableObject) => { + const currTableColumns = tableObject.columns; + + currTableColumns.forEach((columnObject) => { + if (columnObject.checked) { + pixelTables.add(columnObject.tableName); + pixelColumnAliases.push(columnObject.userAlias); + } + }); + }); + + return pixelColumnAliases; + }; + + /** Create a New Cell and Add to Notebook */ + const appendCell = (widget: string) => { + try { + const newCellId = `${Math.floor(Math.random() * 100000)}`; + + const config: NewCellAction['payload']['config'] = { + widget: DefaultCells[widget].widget, + parameters: DefaultCells[widget].parameters, + }; + + if (widget === DataImportCellConfig.widget) { + config.parameters = { + ...DefaultCells[widget].parameters, + frameVariableName: `FRAME_${newCellId}`, + databaseId: selectedDatabaseId, + joins: watchedJoins, + selectQuery: pixelPartialRef.current, + tableNames: Array.from(selectedTableNames), + selectedColumns: getSelectedColumnNames(), + columnAliases: getColumnAliases(), + rootTable: rootTable, + // filters: filters, + }; + } + + if ( + previousCellId && + state.queries[query.id].cells[previousCellId].widget === + widget && + widget === CodeCellConfig.widget + ) { + const previousCellType = + state.queries[query.id].cells[previousCellId].parameters + ?.type ?? 'pixel'; + config.parameters = { + ...DefaultCells[widget].parameters, + type: previousCellType, + }; + } + + state.dispatch({ + message: ActionMessages.NEW_CELL, + payload: { + queryId: query.id, + cellId: newCellId, + previousCellId: previousCellId, + config: config as Omit, + }, + }); + notebook.selectCell(query.id, newCellId); + } catch (e) { + console.error(e); + } + }; + + /** Add all the columns from a Table */ + const addAllTableColumnsHandler = (event) => { + // TODO: check all columns from table + }; + + const updateSubmitDispatches = () => { + const currTableNamesSet = retrieveSelectedTableNames(); + const currTableNames = Array.from(currTableNamesSet); + const currSelectedColumns = retrieveSelectedColumnNames(); + + state.dispatch({ + message: ActionMessages.UPDATE_CELL, + payload: { + queryId: cell.query.id, + cellId: cell.id, + path: 'parameters.tableNames', + value: currTableNames, + }, + }); + + state.dispatch({ + message: ActionMessages.UPDATE_CELL, + payload: { + queryId: cell.query.id, + cellId: cell.id, + path: 'parameters.selectedColumns', + value: currSelectedColumns, + }, + }); + + state.dispatch({ + message: ActionMessages.UPDATE_CELL, + payload: { + queryId: cell.query.id, + cellId: cell.id, + path: 'parameters.columnAliases', + value: getColumnAliases(), + }, + }); + + state.dispatch({ + message: ActionMessages.UPDATE_CELL, + payload: { + queryId: cell.query.id, + cellId: cell.id, + path: 'parameters.joins', + value: joinElements, + }, + }); + + state.dispatch({ + message: ActionMessages.UPDATE_CELL, + payload: { + queryId: cell.query.id, + cellId: cell.id, + path: 'parameters.rootTable', + value: rootTable, + }, + }); + + state.dispatch({ + message: ActionMessages.UPDATE_CELL, + payload: { + queryId: cell.query.id, + cellId: cell.id, + path: 'parameters.selectQuery', + value: pixelPartialRef.current, + }, + }); + + state.dispatch({ + message: ActionMessages.UPDATE_CELL, + payload: { + queryId: cell.query.id, + cellId: cell.id, + path: 'parameters.databaseId', + value: selectedDatabaseId, + }, + }); + + state.dispatch({ + message: ActionMessages.UPDATE_CELL, + payload: { + queryId: cell.query.id, + cellId: cell.id, + path: 'parameters.joins', + value: watchedJoins, + }, + }); + }; + + /** New Submit for Import Data --- empty */ + const onImportDataSubmit = (data: NewFormData) => { + if (editMode) { + retrievePreviewData(); + updatePixelRef(); + updateSubmitDispatches(); + } else { + retrievePreviewData(); + appendCell('data-import'); + } + + closeImportModalHandler(); + setIsDataImportModalOpen(false); + }; + + /** Close and Reset Import Data Form Modal */ + const closeImportModalHandler = () => { + setIsDataImportModalOpen(false); + }; + + /** Get Database Information for Data Import Modal */ + const retrieveDatabaseTablesAndEdges = async (databaseId) => { + setIsDatabaseLoading(true); + const pixelString = `META|GetDatabaseTableStructure(database=[ \"${databaseId}\" ]);META|GetDatabaseMetamodel( database=[ \"${databaseId}\" ], options=["dataTypes","positions"]);`; + + monolithStore.runQuery(pixelString).then((pixelResponse) => { + const responseTableStructure = + pixelResponse.pixelReturn[0].output; + const isResponseTableStructureGood = + pixelResponse.pixelReturn[0].operationType.indexOf( + 'ERROR', + ) === -1; + + const responseTableEdgesStructure = + pixelResponse.pixelReturn[1].output; + const isResponseTableEdgesStructureGood = + pixelResponse.pixelReturn[1].operationType.indexOf( + 'ERROR', + ) === -1; + + let newTableNames = []; + + if (isResponseTableStructureGood) { + newTableNames = [ + ...responseTableStructure.reduce((set, ele) => { + set.add(ele[0]); + return set; + }, new Set()), + ]; + + const tableColumnsObject = responseTableStructure.reduce( + (acc, ele) => { + const tableName = ele[0]; + const columnName = ele[1]; + const columnType = ele[2]; + const columnBoolean = ele[3]; + const columnName2 = ele[4]; + const tableName2 = ele[4]; + + if (!acc[tableName]) acc[tableName] = []; + acc[tableName].push({ + tableName, + columnName, + columnType, + columnBoolean, + columnName2, + tableName2, + userAlias: columnName, + checked: true, + }); + + return acc; + }, + {}, + ); + + const newTableColumnsObject: Table[] = tableColumnsObject + ? Object.keys(tableColumnsObject).map( + (tableName, tableIdx) => ({ + id: tableIdx, + name: tableName, + columns: tableColumnsObject[tableName].map( + (colObj, colIdx) => ({ + id: colIdx, + tableName: tableName, + columnName: colObj.columnName, + columnType: colObj.columnType, + userAlias: colObj.userAlias, + checked: false, + }), + ), + }), + ) + : []; + + formReset({ + databaseSelect: databaseId, + tables: newTableColumnsObject, + }); + } else { + console.error('Error retrieving database tables'); + notification.add({ + color: 'error', + message: `Error retrieving database tables`, + }); + } + + if (isResponseTableEdgesStructureGood) { + const newEdgesDict = + responseTableEdgesStructure.edges.reduce((acc, ele) => { + const source = ele.source; + const target = ele.target; + const sourceColumn = ele.sourceColumn; + const targetColumn = ele.targetColumn; + + if (!acc[source]) { + acc[source] = { + [target]: { + sourceColumn, + targetColumn, + }, + }; + } else { + acc[source][target] = { + sourceColumn, + targetColumn, + }; + } + + if (!acc[target]) { + acc[target] = { + [source]: { + sourceColumn: targetColumn, + targetColumn: sourceColumn, + }, + }; + } else { + acc[target][source] = { + sourceColumn: targetColumn, + targetColumn: sourceColumn, + }; + } + return acc; + }, {}); + + setTableEdgesObject(newEdgesDict); + } else { + console.error('Error retrieving database edges'); + notification.add({ + color: 'error', + message: `Error retrieving database tables`, + }); + } + + const edges = pixelResponse.pixelReturn[1].output.edges; + const newTableEdges = {}; + edges.forEach((edge) => { + if (newTableEdges[edge.source]) { + newTableEdges[edge.source][edge.target] = edge.relation; + } else { + newTableEdges[edge.source] = { + [edge.target]: edge.relation, + }; + } + if (newTableEdges[edge.target]) { + newTableEdges[edge.target][edge.source] = edge.relation; + } else { + newTableEdges[edge.target] = { + [edge.source]: edge.relation, + }; + } + }); + setTableEdges(newTableEdges); + setIsDatabaseLoading(false); + setImportModalPixelWidth(IMPORT_MODAL_WIDTHS.large); + + setTableNames(newTableNames); + + // shown tables filtered only on init load of edit mode + if (editMode && !isInitLoadComplete) { + const newEdges = [ + rootTable, + ...(newTableEdges[rootTable] + ? Object.keys(newTableEdges[rootTable]) + : []), + ]; + setShownTables(new Set(newEdges)); + } else { + setShownTables(new Set(newTableNames)); + } + + if (!editMode || isInitLoadComplete) { + setAliasesCountObj({}); + aliasesCountObjRef.current = {}; + removeJoinElement(); + setJoinsSet(new Set()); + } + }); + + setAliasesCountObj({}); + aliasesCountObjRef.current = {}; + removeJoinElement(); + setIsInitLoadComplete(true); + }; + + /** + * Updates pixel without building preview. + */ + const updatePixelRef = async (): Promise => { + try { + const databaseId = selectedDatabaseId; + const pixelTables: Set = new Set(); + const pixelColumnNames: string[] = []; + const pixelColumnAliases: string[] = []; + const pixelJoins: string[] = []; + + watchedTables?.forEach((tableObject) => { + const currTableColumns = tableObject.columns; + currTableColumns?.forEach((columnObject) => { + if (columnObject.checked) { + pixelTables.add(columnObject.tableName); + pixelColumnNames.push( + `${columnObject.tableName}__${columnObject.columnName}`, + ); + pixelColumnAliases.push(columnObject.userAlias); + } + }); + }); + + watchedJoins?.forEach((joinEle) => { + pixelJoins.push( + `( ${joinEle.leftTable} , ${joinEle.joinType}.join , ${joinEle.rightTable} )`, + ); + }); + + let pixelStringPart1 = `Database ( database = [ \"${databaseId}\" ] )`; + pixelStringPart1 += ` | Select ( ${pixelColumnNames.join( + ' , ', + )} )`; + pixelStringPart1 += `.as ( [ ${pixelColumnAliases.join( + ' , ', + )} ] )`; + if (pixelJoins.length > 0) { + pixelStringPart1 += ` | Join ( ${pixelJoins.join( + ' , ', + )} ) `; + } + pixelStringPart1 += ` | Distinct ( false ) | Limit ( 20 )`; + + const combinedJoinString = + pixelJoins.length > 0 + ? `| Join ( ${pixelJoins.join(' , ')} ) ` + : ''; + + const reactorPixel = `Database ( database = [ \"${databaseId}\" ] ) | Select ( ${pixelColumnNames.join( + ' , ', + )} ) .as ( [ ${pixelColumnAliases.join( + ' , ', + )} ] ) ${combinedJoinString}| Distinct ( false ) | Limit ( 20 ) | Import ( frame = [ CreateFrame ( frameType = [ GRID ] , override = [ true ] ) .as ( [ \"consolidated_settings_FRAME932867__Preview\" ] ) ] ) ; META | Frame() | QueryAll() | Limit(50) | Collect(500);`; + + pixelStringRef.current = reactorPixel; + pixelPartialRef.current = pixelStringPart1 + ';'; + } catch { + setIsDatabaseLoading(false); + setShowTablePreview(false); + setShowEditColumns(true); + + notification.add({ + color: 'error', + message: `Error updating Data Import`, + }); + } + }; + + const retrieveSelectedColumnNames = () => { + const pixelTables = new Set(); + const pixelColumnNames = []; + const pixelColumnAliases = []; + + watchedTables?.forEach((tableObject) => { + const currTableColumns = tableObject.columns; + currTableColumns.forEach((columnObject) => { + if (columnObject.checked) { + pixelTables.add(columnObject.tableName); + pixelColumnNames.push( + `${columnObject.tableName}__${columnObject.columnName}`, + ); + pixelColumnAliases.push(columnObject.userAlias); + } + }); + }); + + return pixelColumnNames; + }; + + const retrieveSelectedTableNames = () => { + const pixelTables = new Set(); + const pixelColumnNames = []; + const pixelColumnAliases = []; + + watchedTables?.forEach((tableObject) => { + const currTableColumns = tableObject.columns; + currTableColumns.forEach((columnObject) => { + if (columnObject.checked) { + pixelTables.add(columnObject.tableName); + pixelColumnNames.push( + `${columnObject.tableName}__${columnObject.columnName}`, + ); + pixelColumnAliases.push(columnObject.userAlias); + } + }); + }); + + return pixelTables; + }; + + const updateSelectedTables = () => { + const pixelTables = new Set(); + const pixelColumnNames = []; + const pixelColumnAliases = []; + + watchedTables?.forEach((tableObject) => { + const currTableColumns = tableObject.columns; + currTableColumns.forEach((columnObject) => { + if (columnObject.checked) { + pixelTables.add(columnObject.tableName); + pixelColumnNames.push( + `${columnObject.tableName}__${columnObject.columnName}`, + ); + pixelColumnAliases.push(columnObject.userAlias); + } + }); + }); + + setSelectedTableNames(pixelTables); + }; + + const retrievePreviewData = async () => { + setIsDatabaseLoading(true); + + const databaseId = selectedDatabaseId; + const pixelTables = new Set(); + const pixelColumnNames = []; + const pixelColumnAliases = []; + const pixelJoins = []; + + try { + watchedTables?.forEach((tableObject) => { + const currTableColumns = tableObject.columns; + currTableColumns?.forEach((columnObject) => { + if (columnObject.checked) { + pixelTables.add(columnObject.tableName); + pixelColumnNames.push( + `${columnObject.tableName}__${columnObject.columnName}`, + ); + pixelColumnAliases.push(columnObject.userAlias); + } + }); + }); + + watchedJoins?.forEach((joinEle) => { + pixelJoins.push( + `( ${joinEle.leftTable} , ${joinEle.joinType}.join , ${joinEle.rightTable} )`, + ); + }); + + let pixelStringPart1 = `Database ( database = [ \"${databaseId}\" ] )`; + pixelStringPart1 += ` | Select ( ${pixelColumnNames.join( + ' , ', + )} )`; + pixelStringPart1 += `.as ( [ ${pixelColumnAliases.join( + ' , ', + )} ] )`; + if (pixelJoins.length > 0) { + pixelStringPart1 += ` | Join ( ${pixelJoins.join( + ' , ', + )} ) `; + } + pixelStringPart1 += ` | Distinct ( false ) | Limit ( 20 )`; + + const combinedJoinString = + pixelJoins.length > 0 + ? `| Join ( ${pixelJoins.join(' , ')} ) ` + : ''; + + const reactorPixel = `Database ( database = [ \"${databaseId}\" ] ) | Select ( ${pixelColumnNames.join( + ' , ', + )} ) .as ( [ ${pixelColumnAliases.join( + ' , ', + )} ] ) ${combinedJoinString}| Distinct ( false ) | Limit ( 20 ) | Import ( frame = [ CreateFrame ( frameType = [ GRID ] , override = [ true ] ) .as ( [ \"consolidated_settings_FRAME932867__Preview\" ] ) ] ) ; META | Frame() | QueryAll() | Limit(50) | Collect(500);`; + + pixelStringRef.current = reactorPixel; + pixelPartialRef.current = pixelStringPart1 + ';'; + + await monolithStore.runQuery(reactorPixel).then((response) => { + const type = response.pixelReturn[0]?.operationType; + const tableHeadersData = + response.pixelReturn[1]?.output?.data?.headers; + const tableRowsData = + response.pixelReturn[1]?.output?.data?.values; + + if (type.indexOf('ERROR') != -1) { + console.error('Error retrieving database tables'); + notification.add({ + color: 'error', + message: `Error retrieving database tables`, + }); + setIsDatabaseLoading(false); + setShowTablePreview(false); + setShowEditColumns(true); + return; + } + + setDatabaseTableHeaders(tableHeadersData); + setDatabaseTableRows(tableRowsData); + setIsDatabaseLoading(false); + }); + } catch { + setIsDatabaseLoading(false); + setShowTablePreview(false); + setShowEditColumns(true); + + notification.add({ + color: 'error', + message: `Error retrieving database tables`, + }); + } + }; + + /** Helper Function Update Alias Tracker Object*/ + const updateAliasCountObj = ( + isBeingAdded, + newAlias, + oldAlias = null, + ) => { + const newAliasesCountObj = { ...aliasesCountObj }; + if (isBeingAdded) { + if (newAliasesCountObj[newAlias] > 0) { + newAliasesCountObj[newAlias] = + newAliasesCountObj[newAlias] + 1; + } else { + newAliasesCountObj[newAlias] = 1; + } + } else { + if (newAliasesCountObj[newAlias] > 0) { + newAliasesCountObj[newAlias] = + newAliasesCountObj[newAlias] - 1; + } else { + newAliasesCountObj[newAlias] = 0; + } + } + + if (newAliasesCountObj[newAlias] < 1) { + delete newAliasesCountObj[newAlias]; + } + if (oldAlias != null) { + if (newAliasesCountObj[oldAlias] > 0) { + newAliasesCountObj[oldAlias] = + newAliasesCountObj[oldAlias] - 1; + } else { + newAliasesCountObj[oldAlias] = 0; + } + + if (newAliasesCountObj[oldAlias] < 1) { + delete newAliasesCountObj[oldAlias]; + } + } + setAliasesCountObj(newAliasesCountObj); + aliasesCountObjRef.current = { ...newAliasesCountObj }; + + updatePixelRef(); // may be unnecessary + }; + + /** Find Joinable Tables */ + const findAllJoinableTables = (rootTableName) => { + const joinableTables = tableEdges[rootTableName] + ? Object.keys(tableEdges[rootTableName]) + : []; + const newShownTables = new Set([...joinableTables, rootTableName]); + setShownTables(newShownTables); + }; + + /** Checkbox Handler */ + const checkBoxHandler = (tableIndex, columnIndex) => { + const columnObject = watchedTables[tableIndex].columns[columnIndex]; + updateAliasCountObj(columnObject?.checked, columnObject.userAlias); + if (columnObject?.checked) { + if (checkedColumnsCount == 0) { + findAllJoinableTables(watchedTables[tableIndex].name); + setRootTable(watchedTables[tableIndex].name); + } + setCheckedColumnsCount(checkedColumnsCount + 1); + } else if (columnObject?.checked == false) { + if (checkedColumnsCount == 1) { + setShownTables(new Set(tableNames)); + setRootTable(null); + } + setCheckedColumnsCount(checkedColumnsCount - 1); + } + setJoinsStackHandler(); + }; + + /** Pre-Populate form For Edit */ + const prepoulateFormForEdit = (cell) => { + const tablesWithCheckedBoxes = new Set(); + const checkedColumns = new Set(); + const columnAliasMap = {}; + const newAliasesCountObj = {}; + + setCheckedColumnsCount(cell.parameters.selectedColumns.length); + + cell.parameters.selectedColumns?.forEach( + (selectedColumnTableCombinedString, idx) => { + const [currTableName, currColumnName] = + selectedColumnTableCombinedString.split('__'); + const currColumnAlias = cell.parameters.columnAliases[idx]; + tablesWithCheckedBoxes.add(currTableName); + checkedColumns.add(selectedColumnTableCombinedString); + columnAliasMap[selectedColumnTableCombinedString] = + currColumnAlias; + newAliasesCountObj[currColumnAlias || currColumnName] = 1; + }, + ); + + setAliasesCountObj({ ...newAliasesCountObj }); + aliasesCountObjRef.current = { ...newAliasesCountObj }; + + if (newTableFields) { + newTableFields?.forEach((newTableObj, tableIdx) => { + if (tablesWithCheckedBoxes.has(newTableObj.name)) { + const watchedTableColumns = + watchedTables[tableIdx].columns; + watchedTableColumns?.forEach( + (tableColumnObj, columnIdx) => { + const columnName = `${tableColumnObj.tableName}__${tableColumnObj.columnName}`; + if (checkedColumns.has(columnName)) { + const columnAlias = + columnAliasMap[columnName]; + formSetValue( + `tables.${tableIdx}.columns.${columnIdx}.checked`, + true, + ); + formSetValue( + `tables.${tableIdx}.columns.${columnIdx}.userAlias`, + columnAlias, + ); + } + }, + ); + } + }); + } + + const newJoinsSet = new Set(); + cell.parameters.joins?.forEach((joinObject) => { + appendJoinElement(joinObject); + const joinsSetString1 = `${joinObject.leftTable}:${joinObject.rightTable}`; + const joinsSetString2 = `${joinObject.rightTable}:${joinObject.leftTable}`; + newJoinsSet.add(joinsSetString1); + newJoinsSet.add(joinsSetString2); + }); + + setJoinsSet(newJoinsSet); + setCheckedColumnsCount(checkedColumns.size); + + const loadedQueryString = cell.parameters.selectQuery; + pixelPartialRef.current = loadedQueryString; + }; + + const checkTableForSelectedColumns = (tableName) => { + for (let i = 0; i < watchedTables.length; i++) { + const currTable = watchedTables[i]; + if (currTable.name == tableName) { + const currTableColumns = currTable.columns; + for (let j = 0; j < currTableColumns.length; j++) { + const currColumn = currTableColumns[j]; + if (currColumn.checked == true) return true; + } + } + } + return false; + }; + + const setJoinsStackHandler = () => { + if (checkedColumnsCount < 2) { + removeJoinElement(); + setJoinsSet(new Set()); + } else { + const leftTable = rootTable; + const rightTables = + tableEdgesObject[rootTable] && + tableEdgesObject && + Object.entries(tableEdgesObject[rootTable]); + + rightTables?.forEach((entry, joinIdx) => { + const rightTable = entry[0]; + const leftKey = entry[1]['sourceColumn']; + const rightKey = entry[1]['targetColumn']; + + const leftTableContainsCheckedColumns = + checkTableForSelectedColumns(leftTable); + const rightTableContainsCheckedColumns = + checkTableForSelectedColumns(rightTable); + + const defaultJoinType = 'inner'; + + const joinsSetString = `${leftTable}:${rightTable}`; + if ( + leftTableContainsCheckedColumns && + rightTableContainsCheckedColumns && + joinsSet.has(joinsSetString) == false + ) { + appendJoinElement({ + leftTable: leftTable, + rightTable: rightTable, + joinType: defaultJoinType, + leftKey: leftKey, + rightKey: rightKey, + }); + addToJoinsSetHelper(joinsSetString); + } else if ( + leftTableContainsCheckedColumns == false || + (rightTableContainsCheckedColumns == false && + joinsSet.has(joinsSetString)) + ) { + joinsSet.delete(joinsSetString); + joinElements.some((ele, idx) => { + if ( + leftTable == ele.leftTable && + rightTable == ele.rightTable && + defaultJoinType == ele.joinType && + leftKey == ele.leftKey && + rightKey == ele.rightKey + ) { + removeJoinElement(idx); + return true; + } else { + return false; + } + }); + } + }); + } + + setInitEditPrepopulateComplete(true); + }; + + const addToJoinsSetHelper = (newJoinSet) => { + const joinsSetCopy = new Set(joinsSet); + joinsSetCopy.add(newJoinSet); + setJoinsSet(joinsSetCopy); + }; + + return ( + + +
+ + + + Import Data from + + ( + { + field.onChange(e.target.value); + setSelectedDatabaseId( + e.target.value, + ); + retrieveDatabaseTablesAndEdges( + e.target.value, + ); + setShowEditColumns(true); + setShowTablePreview(false); + setImportModalPixelWidth( + IMPORT_MODAL_WIDTHS.medium, + ); + }} + label={'Select Database'} + value={field.value || ''} + size={'small'} + disabled={editMode} + > + {userDatabases?.map( + (ele, dbIndex) => ( + + {ele.app_name} + + ), + )} + + )} + /> + + {/* + + */} + + + {isDatabaseLoading && ( + + )} + + {!selectedDatabaseId && ( + + + Select a Database for Import + + + )} + + {selectedDatabaseId && !isDatabaseLoading && ( + + + + + Data + + +
+ { + if (!showEditColumns) { + setShowEditColumns(true); + setShowTablePreview(false); + } + }} + > + Edit Columns + + +
+
+ + {showEditColumns && ( + + + Available Tables / Columns + + + {newTableFields.map( + (table, tableIndex) => ( +
+ {shownTables.has( + table.name, + ) && ( + + + + + + + { + table.name + } + + + + + + + + + + + + + Fields + + + + + Alias + + + + + Field + Type + + + + + {table.columns.map( + ( + column, + columnIndex, + ) => ( + + + ( + { + field.onChange( + e, + ); + checkBoxHandler( + tableIndex, + columnIndex, + ); + }} + /> + )} + /> + + + { + column.columnName + } + {column.columnName == + 'ID' && ( + + PK + + )} + {column.columnName.includes( + '_ID', + ) && ( + + FK + + )} + + + + ( + { + if ( + watchedTables[ + tableIndex + ] + .columns[ + columnIndex + ] + .checked + ) { + updateAliasCountObj( + true, + e + .target + .value, + field.value, + ); + } + field.onChange( + e + .target + .value, + ); + }} + /> + )} + /> + {watchedTables[ + tableIndex + ] + .columns[ + columnIndex + ] + .checked && + aliasesCountObj[ + watchedTables[ + tableIndex + ] + .columns[ + columnIndex + ] + .userAlias + ] > + 1 && ( + + + + )} + + + + + ( + { + field.onChange( + e + .target + .value, + ); + }} + value={ + field.value || + '' + } + size={ + 'small' + } + disabled // TODO enable after adding to form and cell config + > + {SQL_COLUMN_TYPES.map( + ( + ele, + eleIdx, + ) => ( + + { + ele + } + + ), + )} + + )} + /> + + + ), + )} + +
+
+ )} +
+ ), + )} +
+
+ )} + + {showPreview && ( + + + Preview + + + + + + {databaseTableHeaders.map( + (h, hIdx) => ( + + + {h} + + + ), + )} + + {databaseTableRows.map( + (r, rIdx) => ( + + {r.map( + ( + v, + vIdx, + ) => ( + + {v} + + ), + )} + + ), + )} + +
+
+
+ )} +
+ )} + + {joinElements.map((join, joinIndex) => ( + + + + + Join + + + + + {join.leftTable} + + + + + { + setAnchorEl( + e.currentTarget, + ); + setJoinTypeSelectIndex( + joinIndex, + ); + setIsJoinSelectOpen(true); + }} + > + { + JOIN_ICONS[ + watchedJoins[joinIndex] + .joinType + ] + } + + + + {/* Join Select Menu */} + { + setAnchorEl(null); + setIsJoinSelectOpen(false); + setJoinTypeSelectIndex(-1); + }} + > + { + setIsJoinSelectOpen(false); + formSetValue( + `joins.${joinTypeSelectIndex}.joinType`, + 'inner', + ); + }} + > + Inner Join + + { + setIsJoinSelectOpen(false); + formSetValue( + `joins.${joinTypeSelectIndex}.joinType`, + 'left.outer', + ); + }} + > + Left Join + + { + setIsJoinSelectOpen(false); + formSetValue( + `joins.${joinTypeSelectIndex}.joinType`, + 'right.outer', + ); + }} + > + Right Join + + { + setIsJoinSelectOpen(false); + formSetValue( + `joins.${joinTypeSelectIndex}.joinType`, + 'outer', + ); + }} + > + Outer Join + + + + + + {join.rightTable} + + + + + where + + + + + {join.leftKey} + + + + + = + + + + + {join.rightKey} + + + + + + ))} + + + + + + + + + + +
+
+ ); + }, +); diff --git a/packages/client/src/components/notebook/NotebookAddCell.tsx b/packages/client/src/components/notebook/NotebookAddCell.tsx index fe95418b5f..9f1990bb8d 100644 --- a/packages/client/src/components/notebook/NotebookAddCell.tsx +++ b/packages/client/src/components/notebook/NotebookAddCell.tsx @@ -1,7 +1,6 @@ -import { useState, useEffect } from 'react'; +import { useState } from 'react'; import { observer } from 'mobx-react-lite'; -import { computed } from 'mobx'; -import { styled, Button, Divider, Menu, MenuProps, Stack } from '@semoss/ui'; +import { styled, Button, Divider, MenuProps, Menu, Stack } from '@semoss/ui'; import { useBlocks } from '@/hooks'; import { @@ -11,33 +10,28 @@ import { QueryState, } from '@/stores'; import { - AccountTree, - Add, - Functions, ChangeCircleOutlined, - Storage, Code, ImportExport, - TextFields, - KeyboardArrowUp, KeyboardArrowDown, - TableRows, + KeyboardArrowUp, + TextFields, } from '@mui/icons-material'; import { DefaultCellDefinitions, DefaultCells, TransformationCells, - // ImportDataCells, // need options for Import Data dropdown options } from '@/components/cell-defaults'; import { QueryImportCellConfig } from '../cell-defaults/query-import-cell'; import { CodeCellConfig } from '../cell-defaults/code-cell'; +import { DataImportFormModal } from './DataImportFormModal'; const StyledButton = styled(Button)(({ theme }) => ({ color: theme.palette.text.secondary, backgroundColor: 'unset!important', })); -const StyledDivider = styled(Divider)(({ theme }) => ({ +const StyledDivider = styled(Divider)(() => ({ flexGrow: 1, })); @@ -92,6 +86,17 @@ const Transformations = Array.from(Object.values(TransformationCells)).map( }, ); +const DataImportDropdownOptions = [ + { + display: `From Data Catalog`, + defaultCellType: null, + }, + { + display: `From CSV`, + defaultCellType: null, + }, +]; + const AddCellOptions: Record = { code: { display: 'Cell', @@ -101,7 +106,6 @@ const AddCellOptions: Record = { 'query-import': { display: 'Query Import', defaultCellType: 'query-import', - // no DB MUI icon using the icon path from main menu icon: ( = { 'import-data': { display: 'Import Data', icon: , - options: [], - disabled: true, + options: DataImportDropdownOptions, + disabled: false, }, text: { display: 'Text', - // defaultCellType: 'text', // text type currently doesn't exist icon: , disabled: true, }, @@ -149,38 +152,13 @@ export const NotebookAddCell = observer( (props: { query: QueryState; previousCellId?: string }): JSX.Element => { const [anchorEl, setAnchorEl] = useState(null); const [selectedAddCell, setSelectedAddCell] = useState(''); + const [isDataImportModalOpen, setIsDataImportModalOpen] = + useState(false); const open = Boolean(anchorEl); const { query, previousCellId = '' } = props; const { state, notebook } = useBlocks(); - // const cellTypeOptions = computed(() => { - // const options = { ...AddCellOptions }; - // // transformation cell types can only be added if there exists a query-import cell before it - // if (!previousCellId) { - // delete options['transformation']; - // } else { - // const previousCellIndex = query.list.indexOf(previousCellId); - // let hasFrameVariable = false; - // for (let index = 0; index <= previousCellIndex; index++) { - // if ( - // query.cells[query.list[index]].config.widget === - // 'query-import' - // ) { - // hasFrameVariable = true; - // break; - // } - // } - // if (!hasFrameVariable) { - // delete options['transformation']; - // } - // } - - // return Object.values(options); - // }).get(); - - /** - * Create a new cell - */ + /** Create a New Cell and Add to Notebook */ const appendCell = (widget: string) => { try { const newCellId = `${Math.floor(Math.random() * 100000)}`; @@ -229,71 +207,118 @@ export const NotebookAddCell = observer( }; return ( - - - - {Object.entries(AddCellOptions).map((add, i) => { - const value = add[1]; - return ( - { - if (value.options) { - setAnchorEl(e.currentTarget); - setSelectedAddCell(add[0]); - } else { - appendCell(value.defaultCellType); - } - }} - endIcon={ - Array.isArray(value.options) && - (selectedAddCell == add[0] && open ? ( - - ) : ( - - )) - } - > - {value.display} - - ); - })} - - - { - setAnchorEl(null); - }} - > - {Array.from( - AddCellOptions[selectedAddCell]?.options || [], - ({ display, defaultCellType }, index) => { - return ( - { - appendCell(defaultCellType); - setAnchorEl(null); - }} - > - {display} - - ); - }, - )} - - + <> + {/* Dropdown for All Add Cell Option Sets */} + + + + + {AddCellOptions && + Object.entries(AddCellOptions).map((add, i) => { + const value = add[1]; + return ( + { + if (value.options) { + setAnchorEl(e.currentTarget); + setSelectedAddCell(add[0]); + } else { + appendCell( + value.defaultCellType, + ); + } + }} + endIcon={ + Array.isArray(value.options) && + (selectedAddCell == add[0] && + open ? ( + + ) : ( + + )) + } + > + {value.display} + + ); + })} + + + { + setAnchorEl(null); + }} + > + {selectedAddCell === 'transformation' && + Array.from( + AddCellOptions[selectedAddCell]?.options || [], + ({ display, defaultCellType }, index) => { + return ( + { + appendCell(defaultCellType); + setAnchorEl(null); + }} + > + {display} + + ); + }, + )} + + {selectedAddCell === 'import-data' && ( + <> + {Array.from( + AddCellOptions[selectedAddCell]?.options || + [], + ({ display }, index) => { + return ( + { + setIsDataImportModalOpen( + true, + ); + setAnchorEl(null); + }} + > + {display} + + ); + }, + )} + + )} + + + + {isDataImportModalOpen && ( + + )} + ); }, ); diff --git a/packages/client/src/components/notebook/NotebookCell.tsx b/packages/client/src/components/notebook/NotebookCell.tsx index b54ef0574a..f9514637d8 100644 --- a/packages/client/src/components/notebook/NotebookCell.tsx +++ b/packages/client/src/components/notebook/NotebookCell.tsx @@ -746,7 +746,11 @@ export const NotebookCell = observer( )} + {/* { cell.widget == 'data-import' ? +
data import bubbles
+ : */} {rendered} + {/* } */} {cell.isExecuted && ( <>