From d30f9bb0dd2a36059b3a1f663512b0c033c53081 Mon Sep 17 00:00:00 2001 From: neel <9098134+neelneelneel@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:37:18 -0500 Subject: [PATCH 1/3] feat(client): grid wip --- .vscode/semoss.code-workspace | 1 + .../block-defaults/grid-block/GridBlock.tsx | 319 +++++++++++++++ .../grid-block/GridBlockColumnSettings.tsx | 36 ++ .../grid-block/GridBlockSourceSettings.tsx | 59 +++ .../block-defaults/grid-block/config.tsx | 64 +++ .../grid-block/grid-block.types.ts | 73 ++++ .../block-defaults/grid-block/index.ts | 3 + .../src/components/block-defaults/index.ts | 14 +- .../block-defaults/table-block/TableBlock.tsx | 177 --------- .../block-defaults/table-block/config.tsx | 127 ------ .../block-defaults/table-block/index.ts | 2 - .../custom/TableHeaderSettings.tsx | 366 ------------------ .../src/components/block-settings/index.ts | 1 + .../block-settings/shared/JsonSettings.tsx | 21 +- .../cell-defaults/code-cell/config.ts | 1 - packages/client/src/hooks/useBlock.tsx | 12 +- test.json | 226 +++++++++++ 17 files changed, 815 insertions(+), 687 deletions(-) create mode 100644 packages/client/src/components/block-defaults/grid-block/GridBlock.tsx create mode 100644 packages/client/src/components/block-defaults/grid-block/GridBlockColumnSettings.tsx create mode 100644 packages/client/src/components/block-defaults/grid-block/GridBlockSourceSettings.tsx create mode 100644 packages/client/src/components/block-defaults/grid-block/config.tsx create mode 100644 packages/client/src/components/block-defaults/grid-block/grid-block.types.ts create mode 100644 packages/client/src/components/block-defaults/grid-block/index.ts delete mode 100644 packages/client/src/components/block-defaults/table-block/TableBlock.tsx delete mode 100644 packages/client/src/components/block-defaults/table-block/config.tsx delete mode 100644 packages/client/src/components/block-defaults/table-block/index.ts delete mode 100644 packages/client/src/components/block-settings/custom/TableHeaderSettings.tsx create mode 100644 test.json diff --git a/.vscode/semoss.code-workspace b/.vscode/semoss.code-workspace index 9f02a0c2d7..40513d989a 100644 --- a/.vscode/semoss.code-workspace +++ b/.vscode/semoss.code-workspace @@ -36,5 +36,6 @@ "search.exclude": { "**/dist": true }, + "svn.ignoreMissingSvnWarning": true, } } \ No newline at end of file diff --git a/packages/client/src/components/block-defaults/grid-block/GridBlock.tsx b/packages/client/src/components/block-defaults/grid-block/GridBlock.tsx new file mode 100644 index 0000000000..1134174484 --- /dev/null +++ b/packages/client/src/components/block-defaults/grid-block/GridBlock.tsx @@ -0,0 +1,319 @@ +import { useMemo, useState } from 'react'; +import { observer } from 'mobx-react-lite'; + +import { useBlock } from '@/hooks'; +import { BlockComponent } from '@/stores'; +import { + styled, + LinearProgress, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TablePagination, + TableRow, + Menu, +} from '@mui/material'; + +import { GridColumn, GridRow, GridBlockDef } from './grid-block.types'; +import { MenuItem } from '@semoss/ui'; + +const DEFAULT_HEIGHT = '300px'; +const DEFAULT_WIDTH = '500px'; +const DEFAULT_COLUMN_WIDTH = '160px'; + +const StyledBlock = styled('div')(() => ({ + display: 'flex', + flexDirection: 'column', + height: DEFAULT_HEIGHT, + width: DEFAULT_WIDTH, + overflow: 'hidden', +})); + +const StyledTableContainer = styled(TableContainer)(() => ({ + flex: '1', +})); + +const StyledTableHeadRow = styled(TableRow)(() => ({ + color: 'inherit', + backgroundColor: 'inherit', +})); + +const StyledTableHeadCell = styled(TableCell)(() => ({ + textTransform: 'capitalize', + fontWeight: 700, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', +})); + +const StyledTableCell = styled(TableCell)(() => ({ + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', +})); + +export const GridBlock: BlockComponent = observer(({ id }) => { + const { dispatch, attrs, data } = useBlock(id); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + const [contextMenu, setContextMenu] = useState<{ + mouseX: number; + mouseY: number; + column: GridColumn; + row: GridRow; + } | null>(null); + + // get the headers + const columns: GridColumn[] = useMemo(() => { + if (data.source === 'CUSTOM') { + // TODO: Remove this check. This is bad. + try { + if (!data.columns) { + return []; + } else if (typeof data.columns === 'string') { + return JSON.parse(data.columns || []); + } else if (Array.isArray(data.columns)) { + return data.columns; + } + } catch (e) { + // noop + } + + return []; + } else if (data.source === 'FRAME') { + return []; + } + + return []; + }, [data.source, data.columns]); + + // get the values + const values: Record[] = useMemo(() => { + if (data.source === 'CUSTOM') { + // TODO: Remove this conversion. This is bad. + let values = []; + try { + if (!data.values) { + values = []; + } else if (typeof data.values === 'string') { + values = JSON.parse(data.values) as Record< + string, + GridRow + >[]; + } else if (Array.isArray(data.values)) { + values = data.values as Record[]; + } + } catch (e) { + // noop + } + + return values; + } else if (data.source === 'FRAME') { + return []; + } + + return []; + }, [data.source, data.values]); + + // get the rows + const rows: Record[] = useMemo(() => { + if (data.source === 'CUSTOM') { + return values + .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + .map((row) => { + return Object.keys(row).reduce((acc, val) => { + // convert to string + acc[val] = + typeof row[val] === 'string' + ? row[val] + : JSON.stringify(row[val]); + + return acc; + }, {}); + }); + } else if (data.source === 'FRAME') { + return []; + } + + return []; + }, [data.source, values, page, rowsPerPage]); + + // get the count + let count = 0; + if (data.source === 'CUSTOM') { + count = values.length; + } else if (data.source === 'FRAME') { + count == 0; + } + + // get the total width of the table based on the columns + const tableWidth: number = columns.reduce((acc, val) => { + if (val.hidden) { + return acc; + } + + // if it is a number, add it + if (!isNaN(Number(val.width))) { + return acc + Number(val.width); + } + + return acc + parseInt(DEFAULT_COLUMN_WIDTH); + }, 0); + + /** + * Handle the callback for the context menu + * @param event - triggered event + * @param column - selected column + * @param row - value + */ + const handleTableCellOnContextMenu = ( + event: React.MouseEvent, + column: GridColumn, + row: GridRow, + ) => { + // prevent the default interaction + event.preventDefault(); + + // ignore if there are no items + if (!data.contextMenu || data.contextMenu.items.length === 0) { + return; + } + + // open the menu and save the data + setContextMenu( + contextMenu === null + ? { + mouseX: event.clientX + 2, + mouseY: event.clientY - 6, + column: column, + row: row, + } + : // repeated contextmenu when it is already open closes it with Chrome 84 on Ubuntu + // Other native context menus might behave different. + // With this behavior we prevent contextmenu from the backdrop to re-locale existing context menus. + null, + ); + }; + + return ( + + + + + + {columns.map((c) => { + return ( + + {c.name} + + ); + })} + + {data?.loading ? ( + + + + ) : ( + <> + )} + + + {rows.length ? ( + rows.map((r, rIdx) => { + return ( + + {columns.map((c) => { + return ( + + handleTableCellOnContextMenu( + e, + c, + r[c.key], + ) + } + > + {c.hidden ? ( + <>  + ) : ( + r[c.key] + )} + + ); + })} + + ); + }) + ) : ( + +   + + )} + +
+ +
+
+ setContextMenu(null)} + anchorReference="anchorPosition" + anchorPosition={ + contextMenu !== null + ? { top: contextMenu.mouseY, left: contextMenu.mouseX } + : undefined + } + > + {data.contextMenu?.items.map((i, idx) => ( + dispatch(i.action)} + > + {i.name} + + ))} + + setPage(newPage)} + onRowsPerPageChange={(e) => { + setRowsPerPage(parseInt(e.target.value)); + setPage(0); + }} + /> +
+ ); +}); diff --git a/packages/client/src/components/block-defaults/grid-block/GridBlockColumnSettings.tsx b/packages/client/src/components/block-defaults/grid-block/GridBlockColumnSettings.tsx new file mode 100644 index 0000000000..103465397e --- /dev/null +++ b/packages/client/src/components/block-defaults/grid-block/GridBlockColumnSettings.tsx @@ -0,0 +1,36 @@ +import { observer } from 'mobx-react-lite'; + +import { useBlockSettings } from '@/hooks'; + +import { GridBlockDef } from './grid-block.types'; +import { BaseSettingSection, JsonSettings } from '@/components/block-settings'; + +interface GridBlockColumnSettingsProp { + /** Id of the block */ + id: string; +} + +export const GridBlockColumnSettings = observer( + ({ id }: GridBlockColumnSettingsProp) => { + const { data } = useBlockSettings(id); + + return ( + <> + {data.source === 'CUSTOM' ? ( + + + id={id} + path="" + height="300px" + /> + + ) : null} + {data.source === 'FRAME' ? ( + + FrameSelect + + ) : null} + + ); + }, +); diff --git a/packages/client/src/components/block-defaults/grid-block/GridBlockSourceSettings.tsx b/packages/client/src/components/block-defaults/grid-block/GridBlockSourceSettings.tsx new file mode 100644 index 0000000000..d0f32524db --- /dev/null +++ b/packages/client/src/components/block-defaults/grid-block/GridBlockSourceSettings.tsx @@ -0,0 +1,59 @@ +import { observer } from 'mobx-react-lite'; +import { Box, Select } from '@semoss/ui'; + +import { useBlockSettings } from '@/hooks'; + +import { GridBlockDef } from './grid-block.types'; +import { BaseSettingSection, JsonSettings } from '@/components/block-settings'; + +interface GridBlockSourceSettingsProps { + /** Id of the block */ + id: string; +} + +export const GridBlockSourceSettings = observer( + ({ id }: GridBlockSourceSettingsProps) => { + const { data, setData } = useBlockSettings(id); + + /** + * Handle changing of the source + * + * source - new source for the grid + */ + const handleSourceOnChange = (source: 'CUSTOM' | 'FRAME') => { + // update data + setData('source', source); + + if (source === 'CUSTOM') { + // noop + } else if (source === 'FRAME') { + console.warn('TODO ::: Update the context menu'); + } + }; + + return ( + + + + ); + }, +); diff --git a/packages/client/src/components/block-defaults/grid-block/config.tsx b/packages/client/src/components/block-defaults/grid-block/config.tsx new file mode 100644 index 0000000000..955ba11eda --- /dev/null +++ b/packages/client/src/components/block-defaults/grid-block/config.tsx @@ -0,0 +1,64 @@ +import { BlockConfig } from '@/stores'; +import { TableChart } from '@mui/icons-material'; + +import { QuerySelectionSettings } from '@/components/block-settings'; +import { + buildDimensionsSection, + buildColorSection, + buildBorderSection, +} from '../block-defaults.shared'; +import { BLOCK_TYPE_DATA } from '../block-defaults.constants'; + +import { GridBlockDef } from './grid-block.types'; +import { GridBlock } from './GridBlock'; +import { GridBlockColumnSettings } from './GridBlockColumnSettings'; +import { GridBlockSourceSettings } from './GridBlockSourceSettings'; + +// export the config for the block +export const config: BlockConfig = { + widget: 'grid', + type: BLOCK_TYPE_DATA, + data: { + source: 'CUSTOM', + values: [], + columns: [], + contextMenu: { + items: [], + }, + }, + listeners: {}, + slots: {}, + render: GridBlock, + icon: TableChart, + contentMenu: [ + { + name: 'General', + children: [ + { + description: 'Source', + render: ({ id }) => , + }, + { + description: 'Columns', + render: ({ id }) => , + }, + { + description: 'Loading', + render: ({ id }) => ( + + ), + }, + ], + }, + ], + styleMenu: [ + buildDimensionsSection(), + buildColorSection(), + buildBorderSection(), + ], +}; diff --git a/packages/client/src/components/block-defaults/grid-block/grid-block.types.ts b/packages/client/src/components/block-defaults/grid-block/grid-block.types.ts new file mode 100644 index 0000000000..dc36129318 --- /dev/null +++ b/packages/client/src/components/block-defaults/grid-block/grid-block.types.ts @@ -0,0 +1,73 @@ +import React from 'react'; +import { BlockDef, ListenerActions } from '@/stores'; + +/** Column Definition */ +export type GridColumn = { + /** Unique key of the column */ + key: string; + + /** Name of the column */ + name: string; + + /** Width of the column */ + width: string; + + /** Hide the column */ + hidden: boolean; +}; + +/** Row Definition */ +export type GridRow = string; + +/** + * All of the common data attributes + */ +interface AbstractData extends Record { + /** type of data. Is it pulling from a frame or completely custom */ + source: 'FRAME' | 'CUSTOM'; + + /** Column Definitions */ + columns: GridColumn[]; + + /** Track if the table is loading */ + loading?: boolean; + + /** */ + style?: Pick< + React.CSSProperties, + | 'background' + | 'border' + | 'borderColor' + | 'borderStyle' + | 'borderWidth' + | 'height' + | 'width' + >; + + /** */ + contextMenu: { + /** Custom actions */ + items: { + /** Name of the item */ + name: string; + + /** Action that will be triggered */ + action: ListenerActions; + }[]; + }; +} + +interface FrameData extends AbstractData { + source: 'FRAME'; + name: string; +} + +interface CustomData extends AbstractData { + source: 'CUSTOM'; + values: Record[]; +} + +export interface GridBlockDef extends BlockDef<'grid'> { + widget: 'grid'; + data: FrameData | CustomData; +} diff --git a/packages/client/src/components/block-defaults/grid-block/index.ts b/packages/client/src/components/block-defaults/grid-block/index.ts new file mode 100644 index 0000000000..137790a038 --- /dev/null +++ b/packages/client/src/components/block-defaults/grid-block/index.ts @@ -0,0 +1,3 @@ +export * from './grid-block.types'; +export * from './config'; +export * from './GridBlock'; diff --git a/packages/client/src/components/block-defaults/index.ts b/packages/client/src/components/block-defaults/index.ts index 5856ad2f83..e097c62567 100644 --- a/packages/client/src/components/block-defaults/index.ts +++ b/packages/client/src/components/block-defaults/index.ts @@ -24,7 +24,7 @@ import { import { config as HTMLBlockConfig, HTMLBlockDef } from './html-block'; import { config as PageBlockConfig, PageBlockDef } from './page-block'; import { config as SelectBlockConfig, SelectBlockDef } from './select-block'; -import { config as TableBlockConfig, TableBlockDef } from './table-block'; +import { config as GridBlockConfig, GridBlockDef } from './grid-block'; import { config as TextBlockConfig, TextBlockDef } from './text-block'; import { config as InputBlockConfig, InputBlockDef } from './input-block'; import { config as SectionBlockConfig, SectionBlockDef } from './section-block'; @@ -76,7 +76,7 @@ export type DefaultBlockDefinitions = | QueryBlockDef | LogsBlockDef | SelectBlockDef - | TableBlockDef + | GridBlockDef | TextBlockDef | ToggleButtonBlockDef | InputBlockDef @@ -108,6 +108,7 @@ export const DefaultBlocks: Registry = { [LogsBlockConfig.widget]: LogsBlockConfig, [SelectBlockConfig.widget]: SelectBlockConfig, [StepperBlockConfig.widget]: StepperBlockConfig, + [GridBlockConfig.widget]: GridBlockConfig, [TextBlockConfig.widget]: TextBlockConfig, [ToggleButtonBlockConfig.widget]: ToggleButtonBlockConfig, [UploadBlockConfig.widget]: UploadBlockConfig, @@ -116,14 +117,6 @@ export const DefaultBlocks: Registry = { [PDFViewerBlockConfig.widget]: PDFViewerBlockConfig, }; -export function getIconForBlock(widget: string) { - return DefaultBlocks[widget]?.icon; -} - -export function getTypeForBlock(widget: string) { - return DefaultBlocks[widget]?.type; -} - export { AudioBlockConfig, ButtonBlockConfig, @@ -139,6 +132,7 @@ export { LogsBlockConfig, ProgressBlockConfig, SelectBlockConfig, + GridBlockConfig, TextBlockConfig, UploadBlockConfig, VegaVisualizationBlockConfig, diff --git a/packages/client/src/components/block-defaults/table-block/TableBlock.tsx b/packages/client/src/components/block-defaults/table-block/TableBlock.tsx deleted file mode 100644 index 74b98fdc8e..0000000000 --- a/packages/client/src/components/block-defaults/table-block/TableBlock.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { CSSProperties, useMemo, useState } from 'react'; -import { observer } from 'mobx-react-lite'; - -import { useBlock } from '@/hooks'; -import { BlockDef, BlockComponent } from '@/stores'; -import { - styled, - LinearProgress, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TablePagination, - TableRow, -} from '@mui/material'; - -export interface TableBlockDef extends BlockDef<'table'> { - widget: 'table'; - data: { - style: CSSProperties; - content: Array | string; - headers: Array<{ display: string; value: string }>; - noDataText?: string; - loading?: boolean; - }; -} - -const StyledTableRow = styled(TableRow)(({ theme }) => ({ - padding: theme.spacing(2), -})); - -export const TableBlock: BlockComponent = observer(({ id }) => { - const { attrs, data } = useBlock(id); - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(10); - - const content = useMemo(() => { - if (!data?.content) { - return []; - } - - if (typeof data?.content === 'string') { - // try to parse as an array, if not return empty array - try { - const jsonSafeString = data.content.replace(/'/g, '"'); - if (Array.isArray(JSON.parse(jsonSafeString))) { - return JSON.parse(jsonSafeString); - } - } catch (e) { - return []; - } - } - - return Array.isArray(data.content) ? data.content : []; - }, [data?.content]); - - const headerDisplay = useMemo(() => { - if (!data?.headers || data?.headers?.length === 0) { - return content?.length ? Object.keys(content[0]) : []; - } - - return data.headers.map((header) => header.display); - }, [data?.headers, data?.content]); - - const headerValues = useMemo(() => { - if (!data?.headers || data?.headers?.length === 0) { - return content?.length ? Object.keys(content[0]) : []; - } - - return data.headers.map((header) => header.value); - }, [data?.headers, data?.content]); - - return ( -
- - - - - {Array.from(headerDisplay, (header) => { - return ( - - {data?.headers?.length - ? header - : header.replaceAll('_', ' ')} - - ); - })} - - {data?.loading ? ( - - - - ) : ( - <> - )} - - - {content.length ? ( - Array.from( - content.slice( - page * rowsPerPage, - page * rowsPerPage + rowsPerPage, - ), - (row, i) => { - return ( - - {Array.from( - headerValues, - (headerValue, j) => { - return ( - - {row[headerValue] - ? row[ - headerValue - ].toString() - : ''} - - ); - }, - )} - - ); - }, - ) - ) : ( - - - - {data?.noDataText ?? - 'No data available'} - - - - )} - -
- -
-
- setPage(newPage)} - onRowsPerPageChange={(e) => { - setRowsPerPage(parseInt(e.target.value)); - setPage(0); - }} - /> -
- ); -}); diff --git a/packages/client/src/components/block-defaults/table-block/config.tsx b/packages/client/src/components/block-defaults/table-block/config.tsx deleted file mode 100644 index 7a87dfe5c6..0000000000 --- a/packages/client/src/components/block-defaults/table-block/config.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { BlockConfig } from '@/stores'; - -import { - buildLayoutSection, - buildSpacingSection, - buildDimensionsSection, - buildColorSection, - buildTypographySection, - buildBorderSection, -} from '../block-defaults.shared'; - -import { TableBlockDef, TableBlock } from './TableBlock'; -import { TableChart } from '@mui/icons-material'; -import { BLOCK_TYPE_DATA } from '../block-defaults.constants'; -import { TableHeaderSettings } from '@/components/block-settings/custom/TableHeaderSettings'; -import { InputModalSettings } from '@/components/block-settings/shared/InputModalSettings'; -import { - InputSettings, - SelectInputSettings, -} from '@/components/block-settings'; -import { QuerySelectionSettings } from '@/components/block-settings/custom/QuerySelectionSettings'; - -// export the config for the block -export const config: BlockConfig = { - widget: 'table', - type: BLOCK_TYPE_DATA, - data: { - style: { - maxWidth: '100%', - }, - content: [], - headers: [], - }, - listeners: {}, - slots: {}, - render: TableBlock, - icon: TableChart, - contentMenu: [ - { - name: 'General', - children: [ - { - description: 'Columns', - render: ({ id }) => ( - - ), - }, - { - description: 'Content', - render: ({ id }) => ( - - ), - }, - { - description: 'No Data Text', - render: ({ id }) => ( - - ), - }, - { - description: 'Loading', - render: ({ id }) => ( - - ), - }, - ], - }, - ], - styleMenu: [ - buildSpacingSection(), - buildDimensionsSection(), - buildColorSection(), - buildBorderSection(), - { - name: 'Text', - children: [ - { - description: 'Font', - render: ({ id }) => ( - - ), - }, - ], - }, - ], -}; diff --git a/packages/client/src/components/block-defaults/table-block/index.ts b/packages/client/src/components/block-defaults/table-block/index.ts deleted file mode 100644 index 76152c2d88..0000000000 --- a/packages/client/src/components/block-defaults/table-block/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './config'; -export * from './TableBlock'; diff --git a/packages/client/src/components/block-settings/custom/TableHeaderSettings.tsx b/packages/client/src/components/block-settings/custom/TableHeaderSettings.tsx deleted file mode 100644 index 4563115412..0000000000 --- a/packages/client/src/components/block-settings/custom/TableHeaderSettings.tsx +++ /dev/null @@ -1,366 +0,0 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; -import { computed } from 'mobx'; -import { observer } from 'mobx-react-lite'; -import { TextField } from '@semoss/ui'; -import { Paths, PathValue } from '@/types'; -import { useBlockSettings } from '@/hooks'; -import { Block, BlockDef } from '@/stores'; -import { getValueByPath } from '@/utility'; -import { BaseSettingSection } from '../BaseSettingSection'; -import { - Button, - IconButton, - Stack, - ToggleButton, - ToggleButtonGroup, -} from '@mui/material'; -import { Add, Delete, DragIndicator } from '@mui/icons-material'; -import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; - -interface TableHeaderSettingsProps { - /** - * Id of the block that is being worked with - */ - id: string; - - /** - * Path to update - */ - path: Paths['data'], 4>; -} - -export const TableHeaderSettings = observer( - ({ - id, - path, - }: TableHeaderSettingsProps) => { - const { data, setData } = useBlockSettings(id); - - // track the value - const [headers, setHeaders] = useState< - Array<{ display: string; value: string }> - >([{ display: '', value: '' }]); - - const [useCustomHeaders, setUseCustomHeaders] = - useState(false); - - // track the ref to debounce the input - const timeoutRef = useRef>(null); - - // get the value of the input (wrapped in usememo because of path prop) - const computedValue = useMemo(() => { - return computed(() => { - if (!data) { - return [{ display: '', value: '' }]; - } - - const v = getValueByPath(data, path); - if (typeof v === 'undefined') { - return [{ display: '', value: '' }]; - } else if (Array.isArray(v) && v.length) { - return v; - } - - return [{ display: '', value: '' }]; - }); - }, [data, path]).get(); - - // update the value whenever the computed one changes - useEffect(() => { - setHeaders(computedValue); - }, [computedValue]); - - /** - * Sync the data on change - */ - const onChangeCustomHeader = ( - currentHeaders: Array<{ display: string; value: string }>, - headerIndex: number, - display: string, - value: string, - ) => { - // set the value - const newHeaders = [...currentHeaders]; - newHeaders[headerIndex] = { - display: display, - value: value, - }; - setHeaders(newHeaders); - - // clear out the old timeout - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; - } - - timeoutRef.current = setTimeout(() => { - try { - if (display && value) { - // set the value - setData( - path, - newHeaders as PathValue, - ); - } - } catch (e) { - console.log(e); - } - }, 300); - }; - - const onRemoveCustomHeader = (index: number) => { - // set the value - const newHeaders = [ - ...headers.slice(0, index), - ...headers.slice(index + 1), - ]; - setHeaders(newHeaders); - - // clear out the old timeout - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; - } - - timeoutRef.current = setTimeout(() => { - try { - setData( - path, - newHeaders as PathValue, - ); - } catch (e) { - console.log(e); - } - }, 300); - }; - - const onSetAutoHeaderType = () => { - // set the value - setHeaders([{ display: '', value: '' }]); - - // clear out the old timeout - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; - } - - timeoutRef.current = setTimeout(() => { - try { - setData(path, [] as PathValue); - } catch (e) { - console.log(e); - } - }, 300); - }; - - // a little function to help us with reordering the result - const reorder = (startIndex: number, endIndex: number) => { - if (!headers.length) { - return; - } - const newHeaders = Array.from(headers); - const [removed] = newHeaders.splice(startIndex, 1); - newHeaders.splice(endIndex, 0, removed); - - setHeaders(newHeaders); - - // clear out the old timeout - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; - } - - timeoutRef.current = setTimeout(() => { - try { - setData( - path, - newHeaders as PathValue, - ); - } catch (e) { - console.log(e); - } - }, 300); - }; - - return ( - <> - - - setUseCustomHeaders(true)} - > - Custom - - { - setUseCustomHeaders(false); - onSetAutoHeaderType(); - }} - > - Auto - - - - {useCustomHeaders ? ( - <> - { - if (!result.destination) { - return; - } - reorder( - result.source.index, - result.destination.index, - ); - }} - > - - {(provided) => ( - - {Array.from( - headers, - ( - header: { - display: string; - value: string; - }, - i, - ) => { - return ( - - {( - provided, - snapshot, - ) => ( - - { - // sync the data on change - onChangeCustomHeader( - headers, - i, - e - .target - .value, - header.value, - ); - }} - placeholder="Display" - size="small" - variant="outlined" - autoComplete="off" - /> - { - // sync the data on change - onChangeCustomHeader( - headers, - i, - header.display, - e - .target - .value, - ); - }} - placeholder="Value" - size="small" - variant="outlined" - autoComplete="off" - /> - - onRemoveCustomHeader( - i, - ) - } - > - - - - - )} - - ); - }, - )} - {provided.placeholder} - - )} - - - - - - - ) : ( - <> - )} - - ); - }, -); diff --git a/packages/client/src/components/block-settings/index.ts b/packages/client/src/components/block-settings/index.ts index 03742b9716..ccf35dbee9 100644 --- a/packages/client/src/components/block-settings/index.ts +++ b/packages/client/src/components/block-settings/index.ts @@ -1,3 +1,4 @@ export * from './custom'; export * from './shared'; export { ListenerSettings } from './ListenerSettings'; +export { BaseSettingSection } from './BaseSettingSection'; diff --git a/packages/client/src/components/block-settings/shared/JsonSettings.tsx b/packages/client/src/components/block-settings/shared/JsonSettings.tsx index 378c7b9e85..13c495f8cd 100644 --- a/packages/client/src/components/block-settings/shared/JsonSettings.tsx +++ b/packages/client/src/components/block-settings/shared/JsonSettings.tsx @@ -21,10 +21,25 @@ interface JsonSettingsProps { * Path to update */ path: Paths['data'], 4>; + + /** + * Height of the editor + */ + height?: string; + + /** + * Width of the editor + */ + width?: string; } export const JsonSettings = observer( - ({ id, path }: JsonSettingsProps) => { + ({ + id, + path, + height = '100%', + width = '100%', + }: JsonSettingsProps) => { const { data, setData } = useBlockSettings(id); const { state, notebook } = useBlocks(); @@ -234,8 +249,8 @@ export const JsonSettings = observer( return ( ...}> = { }, 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/hooks/useBlock.tsx b/packages/client/src/hooks/useBlock.tsx index 137627d2ca..ca55be67e1 100644 --- a/packages/client/src/hooks/useBlock.tsx +++ b/packages/client/src/hooks/useBlock.tsx @@ -3,7 +3,13 @@ import { computed } from 'mobx'; import { upload } from '@/api'; import { Paths, PathValue } from '@/types'; -import { ActionMessages, Block, BlockDef, ListenerActions } from '@/stores'; +import { + ActionMessages, + Block, + BlockDef, + ListenerActions, + StateStore, +} from '@/stores'; import { copy } from '@/utility'; import { useBlocks } from './useBlocks'; @@ -12,6 +18,9 @@ import { useBlocks } from './useBlocks'; * useBlockReturn */ interface useBlockReturn { + /** Dispatch an action directly */ + dispatch: StateStore['dispatch']; + /** Data for the block */ data: Block['data']; @@ -216,6 +225,7 @@ export const useBlock = ( }).get(); return { + dispatch: state.dispatch, data: data, listeners: listeners, slots: block.slots, diff --git a/test.json b/test.json new file mode 100644 index 0000000000..7a503d3f43 --- /dev/null +++ b/test.json @@ -0,0 +1,226 @@ +{ + "queries": { + "DIABETES_DATA": { + "id": "DIABETES_DATA", + "cells": [ + { + "id": "76370", + "widget": "query-import", + "parameters": { + "databaseId": "950eb187-e352-444d-ad6a-6476ed9390af", + "frameType": "PY", + "frameVariableName": "DIABETES_DATA", + "selectQuery": "Select * from Diabetes" + } + } + ] + }, + "Grid-Config": { + "id": "Grid-Config", + "cells": [ + { + "id": "11609", + "widget": "code", + "parameters": { + "code": "# create the columns\r\ncolumns = [{\"key\": column, \"name\": str(column).replace(\"_\", \" \")} for column in DIABETES_DATA.columns.tolist()]\r\n\r\ncolumns", + "type": "py" + } + }, + { + "id": "45314", + "widget": "code", + "parameters": { + "type": "py", + "code": "# create the values\r\nvalues = DIABETES_DATA.to_json(orient='records');\r\n\r\nvalues" + } + } + ] + } + }, + "blocks": { + "page-1": { + "slots": { + "content": { + "children": [ + "container--8432" + ], + "name": "content" + } + }, + "widget": "page", + "data": { + "style": { + "padding": "24px", + "fontFamily": "roboto", + "flexDirection": "column", + "display": "flex", + "gap": "8px" + } + }, + "listeners": { + "onPageLoad": [ + { + "message": "RUN_QUERY", + "payload": { + "queryId": "DIABETES_DATA" + } + }, + { + "message": "RUN_QUERY", + "payload": { + "queryId": "Grid-Config" + } + } + ] + }, + "id": "page-1" + }, + "text--8697": { + "id": "text--8697", + "widget": "text", + "data": { + "style": { + "padding": "4px", + "whiteSpace": "pre-line", + "textOverflow": "ellipsis", + "textAlign": "left" + }, + "text": "Diabetes Table", + "variant": "h2" + }, + "listeners": {}, + "slots": {} + }, + "container--8432": { + "id": "container--8432", + "widget": "container", + "parent": { + "id": "page-1", + "slot": "content" + }, + "data": { + "style": { + "display": "flex", + "flexDirection": "column", + "padding": "4px", + "gap": "8px", + "flexWrap": "wrap" + } + }, + "listeners": {}, + "slots": { + "children": { + "name": "children", + "children": [ + "text--8697", + "grid-1", + "grid-2" + ] + } + } + }, + "grid-1": { + "id": "grid-1", + "widget": "grid", + "parent": "container--8432", + "data": { + "source": "CUSTOM", + "values": "{{grid-values}}", + "columns": "{{grid-columns}}", + "contextMenu": { + "items": [ + { + "name": "Refresh", + "action": { + "message": "RUN_QUERY", + "payload": { + "queryId": "DIABETES_DATA" + } + } + }, + { + "name": "Filter", + "action": { + "message": "RUN_QUERY", + "payload": { + "queryId": "DIABETES_DATA", + "parameters":{ + "column":"", + "operator":"", + "value": "" + } + } + } + } + ] + }, + "style": { + "width": "100%" + } + }, + "listeners": {}, + "slots": {} + }, + "grid-2": { + "id": "grid-2", + "widget": "grid", + "parent": "container--8432", + "data": { + "source": "CUSTOM", + "values": "{{grid-values}}", + "columns": "{{grid-columns}}", + "contextMenu": { + "items": [ + { + "name":"Refresh", + "action": { + "message": "RUN_QUERY", + "payload": { + "queryId": "DIABETES_DATA" + } + } + }, + { + "name":"Filter", + "action": { + "message": "RUN_QUERY", + "payload": { + "queryId": "DIABETES_DATA", + "parameters":{ + "column":"", + "operator":"", + "value": "" + } + } + } + } + + ] + }, + "style": { + "width": "100%" + } + }, + "listeners": {}, + "slots": {} + } + }, + "variables": { + "grid-columns": { + "type": "cell", + "to": "Grid-Config", + "cellId": "11609" + }, + "grid-values": { + "type": "cell", + "to": "Grid-Config", + "cellId": "45314" + } + }, + "dependencies": {}, + "executionOrder": [ + "DIABETES_DATA", + "Grid-Config" + ], + "version": "1.0.0-alpha.3" +} \ No newline at end of file From e13d109085b29cfac5123f4a32bd561b85bf42ec Mon Sep 17 00:00:00 2001 From: neel <9098134+neelneelneel@users.noreply.github.com> Date: Mon, 18 Nov 2024 09:30:58 -0500 Subject: [PATCH 2/3] feat(client): adding use-frame hook --- .vscode/semoss.code-workspace | 3 + .../block-defaults/grid-block/GridBlock.tsx | 314 ++++++++---------- .../grid-block/GridBlockColumnSettings.tsx | 21 +- .../grid-block/GridBlockSourceSettings.tsx | 59 ---- .../block-defaults/grid-block/config.tsx | 31 +- .../grid-block/grid-block.types.ts | 64 +--- packages/client/src/hooks/index.ts | 4 + packages/client/src/hooks/useBlock.tsx | 12 +- packages/client/src/hooks/useBlocksPixel.tsx | 9 +- packages/client/src/hooks/useFrame.tsx | 74 +++++ packages/client/src/hooks/usePixel.ts | 28 +- test.json | 108 +----- 12 files changed, 270 insertions(+), 457 deletions(-) delete mode 100644 packages/client/src/components/block-defaults/grid-block/GridBlockSourceSettings.tsx create mode 100644 packages/client/src/hooks/useFrame.tsx diff --git a/.vscode/semoss.code-workspace b/.vscode/semoss.code-workspace index 40513d989a..185b48f1b4 100644 --- a/.vscode/semoss.code-workspace +++ b/.vscode/semoss.code-workspace @@ -37,5 +37,8 @@ "**/dist": true }, "svn.ignoreMissingSvnWarning": true, + "typescript.inlayHints.enumMemberValues.enabled": true, + "typescript.tsserver.enableTracing": true, + "typescript.tsserver.experimental.enableProjectDiagnostics": true, } } \ No newline at end of file diff --git a/packages/client/src/components/block-defaults/grid-block/GridBlock.tsx b/packages/client/src/components/block-defaults/grid-block/GridBlock.tsx index 1134174484..fb6b69cbe4 100644 --- a/packages/client/src/components/block-defaults/grid-block/GridBlock.tsx +++ b/packages/client/src/components/block-defaults/grid-block/GridBlock.tsx @@ -1,8 +1,8 @@ -import { useMemo, useState } from 'react'; +import { useState } from 'react'; import { observer } from 'mobx-react-lite'; -import { useBlock } from '@/hooks'; -import { BlockComponent } from '@/stores'; +import { useBlock, useFrame } from '@/hooks'; +import { BlockComponent, BlockDef } from '@/stores'; import { styled, LinearProgress, @@ -16,7 +16,7 @@ import { Menu, } from '@mui/material'; -import { GridColumn, GridRow, GridBlockDef } from './grid-block.types'; +import { GridBlockColumn } from './grid-block.types'; import { MenuItem } from '@semoss/ui'; const DEFAULT_HEIGHT = '300px'; @@ -54,106 +54,64 @@ const StyledTableCell = styled(TableCell)(() => ({ whiteSpace: 'nowrap', })); +export interface GridBlockDef extends BlockDef<'grid'> { + widget: 'grid'; + + /** data associated with the block */ + data: { + /** Bind the grid to a frame */ + frame: { + name: string; + }; + + /** Column Definitions */ + columns: GridBlockColumn[]; + + /** */ + style: Pick< + React.CSSProperties, + | 'background' + | 'border' + | 'borderColor' + | 'borderStyle' + | 'borderWidth' + | 'height' + | 'width' + >; + }; +} + export const GridBlock: BlockComponent = observer(({ id }) => { - const { dispatch, attrs, data } = useBlock(id); + const { attrs, data } = useBlock(id); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); + const [contextMenu, setContextMenu] = useState<{ mouseX: number; mouseY: number; - column: GridColumn; - row: GridRow; + column: GridBlockColumn; + value: unknown; } | null>(null); - // get the headers - const columns: GridColumn[] = useMemo(() => { - if (data.source === 'CUSTOM') { - // TODO: Remove this check. This is bad. - try { - if (!data.columns) { - return []; - } else if (typeof data.columns === 'string') { - return JSON.parse(data.columns || []); - } else if (Array.isArray(data.columns)) { - return data.columns; - } - } catch (e) { - // noop - } - - return []; - } else if (data.source === 'FRAME') { - return []; - } - - return []; - }, [data.source, data.columns]); - - // get the values - const values: Record[] = useMemo(() => { - if (data.source === 'CUSTOM') { - // TODO: Remove this conversion. This is bad. - let values = []; - try { - if (!data.values) { - values = []; - } else if (typeof data.values === 'string') { - values = JSON.parse(data.values) as Record< - string, - GridRow - >[]; - } else if (Array.isArray(data.values)) { - values = data.values as Record[]; - } - } catch (e) { - // noop - } - - return values; - } else if (data.source === 'FRAME') { - return []; - } - - return []; - }, [data.source, data.values]); + // get the frame + const frame = useFrame(data.frame.name, { + limit: rowsPerPage * page, + collect: rowsPerPage, + enableCount: true, + }); - // get the rows - const rows: Record[] = useMemo(() => { - if (data.source === 'CUSTOM') { - return values - .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) - .map((row) => { - return Object.keys(row).reduce((acc, val) => { - // convert to string - acc[val] = - typeof row[val] === 'string' - ? row[val] - : JSON.stringify(row[val]); + // get the columns as as a map + const columnMap: Record = data.columns.reduce( + (acc, val) => { + acc[val.key] = val; - return acc; - }, {}); - }); - } else if (data.source === 'FRAME') { - return []; - } - - return []; - }, [data.source, values, page, rowsPerPage]); - - // get the count - let count = 0; - if (data.source === 'CUSTOM') { - count = values.length; - } else if (data.source === 'FRAME') { - count == 0; - } - - // get the total width of the table based on the columns - const tableWidth: number = columns.reduce((acc, val) => { - if (val.hidden) { return acc; - } + }, + {}, + ); + // get the total width of the table based on the columns + const tableWidth: number = data.columns.reduce((acc, val) => { // if it is a number, add it if (!isNaN(Number(val.width))) { return acc + Number(val.width); @@ -170,17 +128,12 @@ export const GridBlock: BlockComponent = observer(({ id }) => { */ const handleTableCellOnContextMenu = ( event: React.MouseEvent, - column: GridColumn, - row: GridRow, + column: GridBlockColumn, + value: unknown, ) => { // prevent the default interaction event.preventDefault(); - // ignore if there are no items - if (!data.contextMenu || data.contextMenu.items.length === 0) { - return; - } - // open the menu and save the data setContextMenu( contextMenu === null @@ -188,7 +141,7 @@ export const GridBlock: BlockComponent = observer(({ id }) => { mouseX: event.clientX + 2, mouseY: event.clientY - 6, column: column, - row: row, + value: value, } : // repeated contextmenu when it is already open closes it with Chrome 84 on Ubuntu // Other native context menus might behave different. @@ -198,7 +151,7 @@ export const GridBlock: BlockComponent = observer(({ id }) => { }; return ( - + { > - {columns.map((c) => { - return ( - - {c.name} - - ); - })} + {frame.status === 'SUCCESS' + ? frame.data.headers.map((h) => { + // check if the header as a column + const column = columnMap[h]; + if (!column) { + return null; + } + + return ( + + {column.name} + + ); + }) + : null} + {frame.status === 'LOADING' ? ( + + ) : null} - {data?.loading ? ( - - - - ) : ( - <> - )} - {rows.length ? ( - rows.map((r, rIdx) => { - return ( - - {columns.map((c) => { - return ( - - handleTableCellOnContextMenu( - e, - c, - r[c.key], - ) - } - > - {c.hidden ? ( - <>  - ) : ( - r[c.key] - )} - - ); - })} - - ); - }) - ) : ( - -   - - )} + {frame.status === 'SUCCESS' + ? frame.data.values.map((r, rIdx) => { + return ( + + {r.map((v, hIdx) => { + const header = + frame.data.headers[hIdx]; + + // check if the header exists as a column + const column = columnMap[header]; + if (!column) { + return null; + } + + // str for rendering and title + const str = + v !== 'string' + ? JSON.stringify(v) + : v; + + return ( + + handleTableCellOnContextMenu( + e, + column, + v, + ) + } + > + {str} + + ); + })} + + ); + }) + : null}
- -
@@ -291,21 +257,23 @@ export const GridBlock: BlockComponent = observer(({ id }) => { : undefined } > - {data.contextMenu?.items.map((i, idx) => ( + {/* {data.contextMenu?.items.map((i, idx) => ( dispatch(i.action)} + onClick={() => console.log(i.action)} > {i.name} - ))} + ))} */} + {/* TODO: */} + TODO setPage(newPage)} diff --git a/packages/client/src/components/block-defaults/grid-block/GridBlockColumnSettings.tsx b/packages/client/src/components/block-defaults/grid-block/GridBlockColumnSettings.tsx index 103465397e..88355a7240 100644 --- a/packages/client/src/components/block-defaults/grid-block/GridBlockColumnSettings.tsx +++ b/packages/client/src/components/block-defaults/grid-block/GridBlockColumnSettings.tsx @@ -2,7 +2,7 @@ import { observer } from 'mobx-react-lite'; import { useBlockSettings } from '@/hooks'; -import { GridBlockDef } from './grid-block.types'; +import { GridBlockDef } from './GridBlock'; import { BaseSettingSection, JsonSettings } from '@/components/block-settings'; interface GridBlockColumnSettingsProp { @@ -15,22 +15,9 @@ export const GridBlockColumnSettings = observer( const { data } = useBlockSettings(id); return ( - <> - {data.source === 'CUSTOM' ? ( - - - id={id} - path="" - height="300px" - /> - - ) : null} - {data.source === 'FRAME' ? ( - - FrameSelect - - ) : null} - + + + ); }, ); diff --git a/packages/client/src/components/block-defaults/grid-block/GridBlockSourceSettings.tsx b/packages/client/src/components/block-defaults/grid-block/GridBlockSourceSettings.tsx deleted file mode 100644 index d0f32524db..0000000000 --- a/packages/client/src/components/block-defaults/grid-block/GridBlockSourceSettings.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { observer } from 'mobx-react-lite'; -import { Box, Select } from '@semoss/ui'; - -import { useBlockSettings } from '@/hooks'; - -import { GridBlockDef } from './grid-block.types'; -import { BaseSettingSection, JsonSettings } from '@/components/block-settings'; - -interface GridBlockSourceSettingsProps { - /** Id of the block */ - id: string; -} - -export const GridBlockSourceSettings = observer( - ({ id }: GridBlockSourceSettingsProps) => { - const { data, setData } = useBlockSettings(id); - - /** - * Handle changing of the source - * - * source - new source for the grid - */ - const handleSourceOnChange = (source: 'CUSTOM' | 'FRAME') => { - // update data - setData('source', source); - - if (source === 'CUSTOM') { - // noop - } else if (source === 'FRAME') { - console.warn('TODO ::: Update the context menu'); - } - }; - - return ( - - - - ); - }, -); diff --git a/packages/client/src/components/block-defaults/grid-block/config.tsx b/packages/client/src/components/block-defaults/grid-block/config.tsx index 955ba11eda..670a03e677 100644 --- a/packages/client/src/components/block-defaults/grid-block/config.tsx +++ b/packages/client/src/components/block-defaults/grid-block/config.tsx @@ -1,30 +1,24 @@ import { BlockConfig } from '@/stores'; import { TableChart } from '@mui/icons-material'; -import { QuerySelectionSettings } from '@/components/block-settings'; import { buildDimensionsSection, buildColorSection, buildBorderSection, } from '../block-defaults.shared'; import { BLOCK_TYPE_DATA } from '../block-defaults.constants'; - -import { GridBlockDef } from './grid-block.types'; -import { GridBlock } from './GridBlock'; +import { GridBlock, GridBlockDef } from './GridBlock'; import { GridBlockColumnSettings } from './GridBlockColumnSettings'; -import { GridBlockSourceSettings } from './GridBlockSourceSettings'; - // export the config for the block export const config: BlockConfig = { widget: 'grid', type: BLOCK_TYPE_DATA, data: { - source: 'CUSTOM', - values: [], - columns: [], - contextMenu: { - items: [], + frame: { + name: '', }, + columns: [], + style: {}, }, listeners: {}, slots: {}, @@ -34,25 +28,10 @@ export const config: BlockConfig = { { name: 'General', children: [ - { - description: 'Source', - render: ({ id }) => , - }, { description: 'Columns', render: ({ id }) => , }, - { - description: 'Loading', - render: ({ id }) => ( - - ), - }, ], }, ], diff --git a/packages/client/src/components/block-defaults/grid-block/grid-block.types.ts b/packages/client/src/components/block-defaults/grid-block/grid-block.types.ts index dc36129318..92786902ec 100644 --- a/packages/client/src/components/block-defaults/grid-block/grid-block.types.ts +++ b/packages/client/src/components/block-defaults/grid-block/grid-block.types.ts @@ -1,8 +1,5 @@ -import React from 'react'; -import { BlockDef, ListenerActions } from '@/stores'; - /** Column Definition */ -export type GridColumn = { +export type GridBlockColumn = { /** Unique key of the column */ key: string; @@ -11,63 +8,4 @@ export type GridColumn = { /** Width of the column */ width: string; - - /** Hide the column */ - hidden: boolean; }; - -/** Row Definition */ -export type GridRow = string; - -/** - * All of the common data attributes - */ -interface AbstractData extends Record { - /** type of data. Is it pulling from a frame or completely custom */ - source: 'FRAME' | 'CUSTOM'; - - /** Column Definitions */ - columns: GridColumn[]; - - /** Track if the table is loading */ - loading?: boolean; - - /** */ - style?: Pick< - React.CSSProperties, - | 'background' - | 'border' - | 'borderColor' - | 'borderStyle' - | 'borderWidth' - | 'height' - | 'width' - >; - - /** */ - contextMenu: { - /** Custom actions */ - items: { - /** Name of the item */ - name: string; - - /** Action that will be triggered */ - action: ListenerActions; - }[]; - }; -} - -interface FrameData extends AbstractData { - source: 'FRAME'; - name: string; -} - -interface CustomData extends AbstractData { - source: 'CUSTOM'; - values: Record[]; -} - -export interface GridBlockDef extends BlockDef<'grid'> { - widget: 'grid'; - data: FrameData | CustomData; -} diff --git a/packages/client/src/hooks/index.ts b/packages/client/src/hooks/index.ts index 9f77a5ffac..611512fb4f 100644 --- a/packages/client/src/hooks/index.ts +++ b/packages/client/src/hooks/index.ts @@ -1,8 +1,10 @@ import { useAPI } from './useAPI'; import { useBlock } from './useBlock'; import { useBlocks } from './useBlocks'; +import { useBlocksPixel } from './useBlocksPixel'; import { useBlockSettings } from './useBlockSettings'; import { useEngine } from './useEngine'; +import { useFrame } from './useFrame'; import { useLLM } from './useLLM'; import { useMetamodel } from './useMetamodel'; import { useRootStore } from './useRootStore'; @@ -20,8 +22,10 @@ export { useAPI, useBlock, useBlocks, + useBlocksPixel, useBlockSettings, useEngine, + useFrame, useLLM, useMetamodel, useRootStore, diff --git a/packages/client/src/hooks/useBlock.tsx b/packages/client/src/hooks/useBlock.tsx index ca55be67e1..137627d2ca 100644 --- a/packages/client/src/hooks/useBlock.tsx +++ b/packages/client/src/hooks/useBlock.tsx @@ -3,13 +3,7 @@ import { computed } from 'mobx'; import { upload } from '@/api'; import { Paths, PathValue } from '@/types'; -import { - ActionMessages, - Block, - BlockDef, - ListenerActions, - StateStore, -} from '@/stores'; +import { ActionMessages, Block, BlockDef, ListenerActions } from '@/stores'; import { copy } from '@/utility'; import { useBlocks } from './useBlocks'; @@ -18,9 +12,6 @@ import { useBlocks } from './useBlocks'; * useBlockReturn */ interface useBlockReturn { - /** Dispatch an action directly */ - dispatch: StateStore['dispatch']; - /** Data for the block */ data: Block['data']; @@ -225,7 +216,6 @@ export const useBlock = ( }).get(); return { - dispatch: state.dispatch, data: data, listeners: listeners, slots: block.slots, diff --git a/packages/client/src/hooks/useBlocksPixel.tsx b/packages/client/src/hooks/useBlocksPixel.tsx index a4356a2742..6e98df8daf 100644 --- a/packages/client/src/hooks/useBlocksPixel.tsx +++ b/packages/client/src/hooks/useBlocksPixel.tsx @@ -1,17 +1,20 @@ import { useContext } from 'react'; import { BlocksContext } from '@/contexts'; -import { usePixel } from './usePixel'; +import { PixelConfig, usePixel } from './usePixel'; /** * Run pixel within blocks context * @returns Pixel response */ -export function useBlocksPixel(pixel: string) { +export function useBlocksPixel( + pixel: string, + config?: Partial>, +) { const context = useContext(BlocksContext); if (context === undefined) { throw new Error('useBlocksPixel must be used within Blocks'); } - return usePixel(pixel, undefined, context.state.insightId); + return usePixel(pixel, config, context.state.insightId); } diff --git a/packages/client/src/hooks/useFrame.tsx b/packages/client/src/hooks/useFrame.tsx new file mode 100644 index 0000000000..0c4bad5cc0 --- /dev/null +++ b/packages/client/src/hooks/useFrame.tsx @@ -0,0 +1,74 @@ +import { useBlocksPixel } from './useBlocksPixel'; + +/** + * Run pixel within blocks context + * @returns Pixel response + */ +export function useFrame( + /** Frame to get data from */ + frame = '', + + /** Options for the frame */ + options?: Partial<{ + /** Selector to grab the data */ + selector: string; + + /** Where to start grabbing the data */ + limit: number; + + /** How many to collect */ + collect: number; + + /** Enable the count */ + enableCount: boolean; + }>, +) { + const { + selector = 'QueryAll()', + limit = 0, + collect = 1000, + enableCount = false, + } = options; + + /** + * Get the data + */ + const getData = useBlocksPixel<{ + data: { + headers: string[]; + values: unknown[][]; + }; + }>( + frame + ? `META | Frame("${frame}") | ${selector} | Limit(${limit}) | Collect(${collect});` + : '', + { + silent: true, + }, + ); + + /** + * Get the count of all of the values + */ + const getCount = useBlocksPixel( + enableCount && frame + ? `META | Frame("${frame}") | ${selector} | Distinct(false) | QueryRowCount();` + : '', + { + silent: true, + }, + ); + + return { + status: getData.status, + data: + getData.status === 'SUCCESS' + ? getData.data.data + : { + headers: [], + values: [], + }, + error: getData.error, + count: getCount.status === 'SUCCESS' ? getCount.data : -1, + }; +} diff --git a/packages/client/src/hooks/usePixel.ts b/packages/client/src/hooks/usePixel.ts index 170c2894d7..22cca4a597 100644 --- a/packages/client/src/hooks/usePixel.ts +++ b/packages/client/src/hooks/usePixel.ts @@ -15,6 +15,9 @@ interface PixelState { export interface PixelConfig { /** Initial Data */ data: D; + + /** Mangually process errors. Does not throw notifications */ + silent: boolean; } interface usePixel extends PixelState { @@ -43,6 +46,7 @@ export function usePixel( const options: PixelConfig = useMemo(() => { return { data: undefined, + silent: false, ...config, }; }, [config]); @@ -114,17 +118,7 @@ export function usePixel( if (operationType.indexOf('ERROR') > -1) { const error = output as string; - notification.add({ - color: 'error', - message: error, - }); - - setState({ - status: 'ERROR', - error: Error(error), - }); - - return; + throw new Error(error); } // set as success @@ -139,10 +133,14 @@ export function usePixel( return; } - notification.add({ - color: 'error', - message: error.message, - }); + if (!options.silent) { + notification.add({ + color: 'error', + message: error.message, + }); + } else { + console.log(error.message); + } setState({ status: 'ERROR', diff --git a/test.json b/test.json index 7a503d3f43..70dc25c748 100644 --- a/test.json +++ b/test.json @@ -41,9 +41,7 @@ "page-1": { "slots": { "content": { - "children": [ - "container--8432" - ], + "children": ["container--8432"], "name": "content" } }, @@ -111,11 +109,7 @@ "slots": { "children": { "name": "children", - "children": [ - "text--8697", - "grid-1", - "grid-2" - ] + "children": ["text--8697", "grid-1", "grid-2"] } } }, @@ -124,39 +118,14 @@ "widget": "grid", "parent": "container--8432", "data": { - "source": "CUSTOM", - "values": "{{grid-values}}", - "columns": "{{grid-columns}}", - "contextMenu": { - "items": [ - { - "name": "Refresh", - "action": { - "message": "RUN_QUERY", - "payload": { - "queryId": "DIABETES_DATA" - } - } - }, - { - "name": "Filter", - "action": { - "message": "RUN_QUERY", - "payload": { - "queryId": "DIABETES_DATA", - "parameters":{ - "column":"", - "operator":"", - "value": "" - } - } - } - } - ] + "frame": { + "name": "DIABETES_DATA" }, - "style": { - "width": "100%" - } + "columns": [ + { "key": "ID", "name": "id" }, + { "key": "DRUG", "name": "Drug" } + ], + "style": {} }, "listeners": {}, "slots": {} @@ -166,61 +135,20 @@ "widget": "grid", "parent": "container--8432", "data": { - "source": "CUSTOM", - "values": "{{grid-values}}", - "columns": "{{grid-columns}}", - "contextMenu": { - "items": [ - { - "name":"Refresh", - "action": { - "message": "RUN_QUERY", - "payload": { - "queryId": "DIABETES_DATA" - } - } - }, - { - "name":"Filter", - "action": { - "message": "RUN_QUERY", - "payload": { - "queryId": "DIABETES_DATA", - "parameters":{ - "column":"", - "operator":"", - "value": "" - } - } - } - } - - ] + "frame": { + "name": "DIABETES_DATA" }, - "style": { - "width": "100%" - } + "columns": [ + { "key": "DRUG", "name": "Drug" } + ], + "style": {} }, "listeners": {}, "slots": {} } }, - "variables": { - "grid-columns": { - "type": "cell", - "to": "Grid-Config", - "cellId": "11609" - }, - "grid-values": { - "type": "cell", - "to": "Grid-Config", - "cellId": "45314" - } - }, + "variables": {}, "dependencies": {}, - "executionOrder": [ - "DIABETES_DATA", - "Grid-Config" - ], + "executionOrder": ["DIABETES_DATA", "Grid-Config"], "version": "1.0.0-alpha.3" -} \ No newline at end of file +} From 3057a6715086d965d4cfed8f842bd1ec7c55ab6d Mon Sep 17 00:00:00 2001 From: neel <9098134+neelneelneel@users.noreply.github.com> Date: Mon, 18 Nov 2024 23:05:06 -0500 Subject: [PATCH 3/3] feat(client): adding right menu --- .vscode/semoss.code-workspace | 4 - .../block-defaults/grid-block/GridBlock.tsx | 217 +++++++++--------- .../grid-block/GridBlockColumnSettings.tsx | 184 ++++++++++++++- .../GridBlockColumnSettingsItem.tsx | 80 +++++++ .../grid-block/GridBlockContextMenu.tsx | 86 +++++++ .../grid-block/grid-block.types.ts | 6 +- packages/client/src/hooks/index.ts | 2 + packages/client/src/hooks/useFrame.tsx | 111 +++++++-- packages/client/src/hooks/useFrameHeaders.tsx | 81 +++++++ .../client/src/stores/state/cell.state.ts | 161 +++++++------ .../client/src/stores/state/query.state.ts | 23 +- .../client/src/stores/state/state.store.ts | 102 +++++++- .../client/src/stores/state/state.types.ts | 11 + packages/ui/src/components/List/List.tsx | 26 ++- packages/ui/src/components/List/ListItem.tsx | 40 ++-- .../ui/src/components/List/ListItemText.tsx | 46 +--- 16 files changed, 875 insertions(+), 305 deletions(-) create mode 100644 packages/client/src/components/block-defaults/grid-block/GridBlockColumnSettingsItem.tsx create mode 100644 packages/client/src/components/block-defaults/grid-block/GridBlockContextMenu.tsx create mode 100644 packages/client/src/hooks/useFrameHeaders.tsx diff --git a/.vscode/semoss.code-workspace b/.vscode/semoss.code-workspace index 185b48f1b4..9f02a0c2d7 100644 --- a/.vscode/semoss.code-workspace +++ b/.vscode/semoss.code-workspace @@ -36,9 +36,5 @@ "search.exclude": { "**/dist": true }, - "svn.ignoreMissingSvnWarning": true, - "typescript.inlayHints.enumMemberValues.enabled": true, - "typescript.tsserver.enableTracing": true, - "typescript.tsserver.experimental.enableProjectDiagnostics": true, } } \ No newline at end of file diff --git a/packages/client/src/components/block-defaults/grid-block/GridBlock.tsx b/packages/client/src/components/block-defaults/grid-block/GridBlock.tsx index fb6b69cbe4..e2ff4530b7 100644 --- a/packages/client/src/components/block-defaults/grid-block/GridBlock.tsx +++ b/packages/client/src/components/block-defaults/grid-block/GridBlock.tsx @@ -13,11 +13,10 @@ import { TableHead, TablePagination, TableRow, - Menu, } from '@mui/material'; import { GridBlockColumn } from './grid-block.types'; -import { MenuItem } from '@semoss/ui'; +import { GridBlockContextMenu } from './GridBlockContextMenu'; const DEFAULT_HEIGHT = '300px'; const DEFAULT_WIDTH = '500px'; @@ -78,6 +77,15 @@ export interface GridBlockDef extends BlockDef<'grid'> { | 'height' | 'width' >; + + /** Context Menu */ + contextMenu?: { + /** Show the unfilter related options */ + hideUnfilter: boolean; + + /** Show the filter related options */ + hideFilter: boolean; + }; }; } @@ -93,17 +101,29 @@ export const GridBlock: BlockComponent = observer(({ id }) => { value: unknown; } | null>(null); + // create the selector + const selector = `Select(${data.columns + .map((c) => { + return c.selector; + }) + .join(', ')}).as([${data.columns + .map((c) => { + return c.name; + }) + .join(', ')}])`; + // get the frame const frame = useFrame(data.frame.name, { - limit: rowsPerPage * page, - collect: rowsPerPage, + selector: selector, + offset: rowsPerPage * page, + limit: rowsPerPage, enableCount: true, }); - // get the columns as as a map - const columnMap: Record = data.columns.reduce( - (acc, val) => { - acc[val.key] = val; + // get the headers as as a map (header -> idx) + const headerMap: Record = frame.data.headers.reduce( + (acc, val, idx) => { + acc[val] = idx; return acc; }, @@ -160,116 +180,97 @@ export const GridBlock: BlockComponent = observer(({ id }) => { > - {frame.status === 'SUCCESS' - ? frame.data.headers.map((h) => { - // check if the header as a column - const column = columnMap[h]; - if (!column) { - return null; - } - - return ( - - {column.name} - - ); - }) - : null} - {frame.status === 'LOADING' ? ( - - ) : null} + {data.columns.map((c, cIdx) => { + return ( + + {c.name} + + ); + })} - {frame.status === 'SUCCESS' - ? frame.data.values.map((r, rIdx) => { - return ( - - {r.map((v, hIdx) => { - const header = - frame.data.headers[hIdx]; + {frame.isLoading ? ( + + ) : ( + frame.data.values.map((r, rIdx) => { + return ( + + {data.columns.map((c, cIdx) => { + let headerExists = false; + // check if the header exists + if ( + Object.prototype.hasOwnProperty.call( + headerMap, + c.name, + ) + ) { + headerExists = true; + } + + // get the value + const value = r[headerMap[c.name]]; - // check if the header exists as a column - const column = columnMap[header]; - if (!column) { - return null; - } + // str for rendering and title + const str = + typeof value !== 'string' + ? JSON.stringify(value) + : value; - // str for rendering and title - const str = - v !== 'string' - ? JSON.stringify(v) - : v; + return ( + { + // don't open context menu + if (!headerExists) { + return; + } - return ( - - handleTableCellOnContextMenu( - e, - column, - v, - ) - } - > - {str} - - ); - })} - - ); - }) - : null} + handleTableCellOnContextMenu( + e, + c, + value, + ); + }} + > + {str} + + ); + })} + + ); + }) + )} - setContextMenu(null)} - anchorReference="anchorPosition" - anchorPosition={ - contextMenu !== null - ? { top: contextMenu.mouseY, left: contextMenu.mouseX } - : undefined - } - > - {/* {data.contextMenu?.items.map((i, idx) => ( - console.log(i.action)} - > - {i.name} - - ))} */} - {/* TODO: */} - TODO - + /> { - const { data } = useBlockSettings(id); + ({ id }: GridBlockColumnSettingsProps) => { + const notification = useNotification(); + const { data, setData } = useBlockSettings(id); + + // get all of the frames + const getFrames = useBlocksPixel('GetFrames();', { + data: [], + }); + + // get headers associated with the selected frames + const frameHeaders = useFrameHeaders(data.frame.name); + + /** + * Sync the columns with the frame headers + */ + const syncFrameHeaders = () => { + try { + // get the columns by selector + const columnMap: Record = + data.columns.reduce((acc, val) => { + acc[val.name] = acc; + + return acc; + }, {}); + + // get the frameHeaders as columns + const columns: GridBlockColumn[] = frameHeaders.data.list.map( + (h) => { + return { + name: h.alias, + width: undefined, + // add the previous if it exists + ...JSON.parse( + JSON.stringify(columnMap[h.alias] || {}), + ), + selector: h.header, + }; + }, + ); + + // update the data + setData('columns', columns); + + notification.add({ + color: 'success', + message: 'Succesfully synchronized headers', + }); + } catch (e) { + notification.add({ + color: 'error', + message: e.message, + }); + } + }; + + /** + * Reorder columns + * @param startDragIndex + * @param stopDragIndex + */ + const reorderColumns = ( + startDragIndex: number, + stopDragIndex: number, + ) => { + // get the columns + const columns = [...data.columns]; + + // remove it + const [removed] = columns.splice(startDragIndex, 1); + + // add it at the new location + columns.splice(stopDragIndex, 0, removed); + + // update the data + setData('columns', columns); + }; + + // options for the autocomplete + const options = getFrames.status === 'SUCCESS' ? getFrames.data : []; + + // columns to render + const columns = data.columns || []; return ( - - - + <> + + { + return option; + }} + onChange={(_, value) => { + // update the frame + setData('frame.name', value); + }} + freeSolo={false} + renderInput={(params) => ( + + )} + /> + + syncFrameHeaders()}> + + + + + { + // ingnore if no destination + if (!result.destination) { + return; + } + + // swap + reorderColumns( + result.source.index, + result.destination.index, + ); + }} + > + + {(provided) => ( + + {columns.map((c, cIdx) => { + return ( + + ); + })} + {/* + + */} + + )} + + + + ); }, ); diff --git a/packages/client/src/components/block-defaults/grid-block/GridBlockColumnSettingsItem.tsx b/packages/client/src/components/block-defaults/grid-block/GridBlockColumnSettingsItem.tsx new file mode 100644 index 0000000000..1899a0e58a --- /dev/null +++ b/packages/client/src/components/block-defaults/grid-block/GridBlockColumnSettingsItem.tsx @@ -0,0 +1,80 @@ +import { observer } from 'mobx-react-lite'; +import { Draggable } from 'react-beautiful-dnd'; + +import { IconButton, List } from '@semoss/ui'; +import { useBlockSettings } from '@/hooks'; + +import { GridBlockDef } from './GridBlock'; +import { Delete } from '@mui/icons-material'; +import { GridBlockColumn } from './grid-block.types'; + +interface GridBlockColumnSettingsItemProps { + /** Id of the block */ + id: string; + + /** Index of the column */ + column: GridBlockColumn; + + /** Index of the column */ + index: number; +} + +export const GridBlockColumnSettingsItem = observer( + ({ id, column, index }: GridBlockColumnSettingsItemProps) => { + const { data, setData } = useBlockSettings(id); + + return ( + + {(provided, snapshot) => ( + + { + // get the columns except the current one + const columns = data.columns.filter( + (v, idx) => index !== idx, + ); + + // update the data + setData('columns', columns); + }} + > + + + + } + > + + + )} + + ); + }, +); diff --git a/packages/client/src/components/block-defaults/grid-block/GridBlockContextMenu.tsx b/packages/client/src/components/block-defaults/grid-block/GridBlockContextMenu.tsx new file mode 100644 index 0000000000..70286756e5 --- /dev/null +++ b/packages/client/src/components/block-defaults/grid-block/GridBlockContextMenu.tsx @@ -0,0 +1,86 @@ +import { observer } from 'mobx-react-lite'; +import { Menu, MenuItem } from '@mui/material'; + +import { useBlock, useFrame } from '@/hooks'; + +import { GridBlockColumn } from './grid-block.types'; +import { GridBlockDef } from './GridBlock'; + +export interface GridBlockContextMenuProps { + /** ID of the block */ + id: string; + + /** Frame that the user is interacting with */ + frame: ReturnType; + + /** Context Menu */ + contextMenu: { + mouseX: number; + mouseY: number; + column: GridBlockColumn; + value: unknown; + } | null; + + /** Close the context menu */ + onClose: () => void; +} + +export const GridBlockContextMenu: React.FC = + observer( + ({ + id = '', + frame = null, + contextMenu = null, + onClose = () => null, + }) => { + const { data } = useBlock(id); + + return ( + onClose()} + anchorReference="anchorPosition" + anchorPosition={ + contextMenu !== null + ? { + top: contextMenu.mouseY, + left: contextMenu.mouseX, + } + : undefined + } + > + {contextMenu && !data.contextMenu?.hideUnfilter ? ( + { + frame.unfilter(); + onClose(); + }} + > + Unfilter + + ) : null} + {contextMenu && !data.contextMenu?.hideFilter ? ( + { + frame.filter( + `SetFrameFilter(${ + contextMenu.column.selector + }==${JSON.stringify(contextMenu.value)})`, + ); + onClose(); + }} + > + Filter {contextMenu.column.name} == + {typeof contextMenu.value === 'string' + ? contextMenu.value + : JSON.stringify(contextMenu.value)} + + ) : null} + + ); + }, + ); diff --git a/packages/client/src/components/block-defaults/grid-block/grid-block.types.ts b/packages/client/src/components/block-defaults/grid-block/grid-block.types.ts index 92786902ec..4baad888e6 100644 --- a/packages/client/src/components/block-defaults/grid-block/grid-block.types.ts +++ b/packages/client/src/components/block-defaults/grid-block/grid-block.types.ts @@ -1,11 +1,11 @@ /** Column Definition */ export type GridBlockColumn = { - /** Unique key of the column */ - key: string; - /** Name of the column */ name: string; + /** Selector for the column */ + selector: string; + /** Width of the column */ width: string; }; diff --git a/packages/client/src/hooks/index.ts b/packages/client/src/hooks/index.ts index 611512fb4f..e454bb1a39 100644 --- a/packages/client/src/hooks/index.ts +++ b/packages/client/src/hooks/index.ts @@ -5,6 +5,7 @@ import { useBlocksPixel } from './useBlocksPixel'; import { useBlockSettings } from './useBlockSettings'; import { useEngine } from './useEngine'; import { useFrame } from './useFrame'; +import { useFrameHeaders } from './useFrameHeaders'; import { useLLM } from './useLLM'; import { useMetamodel } from './useMetamodel'; import { useRootStore } from './useRootStore'; @@ -26,6 +27,7 @@ export { useBlockSettings, useEngine, useFrame, + useFrameHeaders, useLLM, useMetamodel, useRootStore, diff --git a/packages/client/src/hooks/useFrame.tsx b/packages/client/src/hooks/useFrame.tsx index 0c4bad5cc0..132653df18 100644 --- a/packages/client/src/hooks/useFrame.tsx +++ b/packages/client/src/hooks/useFrame.tsx @@ -1,66 +1,148 @@ -import { useBlocksPixel } from './useBlocksPixel'; +import { useCallback, useContext, useEffect, useState } from 'react'; +import { BlocksContext } from '@/contexts'; + +import { usePixel } from './usePixel'; /** - * Run pixel within blocks context - * @returns Pixel response + * Use a frame's in an insight + * + * @param frame + * @param options */ export function useFrame( /** Frame to get data from */ frame = '', /** Options for the frame */ - options?: Partial<{ + options: Partial<{ /** Selector to grab the data */ selector: string; /** Where to start grabbing the data */ - limit: number; + offset: number; /** How many to collect */ - collect: number; + limit: number; /** Enable the count */ enableCount: boolean; - }>, + }> = {}, ) { const { selector = 'QueryAll()', - limit = 0, - collect = 1000, + offset = 0, + limit = 1000, enableCount = false, } = options; + const [isLoading, setIsLoading] = useState(false); + + const context = useContext(BlocksContext); + if (context === undefined) { + throw new Error('useFrame must be used within Blocks'); + } + + const { state } = context; + + // get the frameKey, this will change whenever the data does + const frameKey = state.getFrameKey(frame); + + /** + * Filter the frame + * + * @param - filterPixel + */ + const filterFrame = useCallback( + async (filterPixel: string): Promise => { + try { + setIsLoading(true); + + // filter the frame + const response = await state.runSideEffect( + `META | Frame("${frame}") | ${filterPixel};`, + ); + + console.group(response); + + return true; + } catch (e) { + // log the error + console.error(e); + return false; + } finally { + setIsLoading(false); + } + }, + [frame], + ); + + /** + * Filter the frame + * + * @param - filterPixel + */ + const unfilterFrame = useCallback(async (): Promise => { + try { + setIsLoading(true); + + // filter the frame + const response = await state.runSideEffect( + `META | UnfilterFrame("${frame}");`, + ); + + console.group(response); + + return true; + } catch (e) { + // log the error + console.error(e); + return false; + } finally { + setIsLoading(false); + } + }, []); + /** * Get the data */ - const getData = useBlocksPixel<{ + const getData = usePixel<{ data: { headers: string[]; values: unknown[][]; }; }>( frame - ? `META | Frame("${frame}") | ${selector} | Limit(${limit}) | Collect(${collect});` + ? `META | Frame("${frame}") | ${selector} | Offset(${offset}) ${ + limit !== -1 ? `| Limit(${limit})` : '' + } | Collect(${limit});` : '', { silent: true, }, + context.state.insightId, ); /** * Get the count of all of the values */ - const getCount = useBlocksPixel( + const getCount = usePixel( enableCount && frame ? `META | Frame("${frame}") | ${selector} | Distinct(false) | QueryRowCount();` : '', { silent: true, }, + context.state.insightId, ); + // refresh the data whenever the key changes + useEffect(() => { + getData.refresh(); + getCount.refresh(); + }, [frameKey]); + return { - status: getData.status, + isLoading: isLoading || getData.status === 'LOADING', data: getData.status === 'SUCCESS' ? getData.data.data @@ -70,5 +152,8 @@ export function useFrame( }, error: getData.error, count: getCount.status === 'SUCCESS' ? getCount.data : -1, + /** Actions */ + filter: filterFrame, + unfilter: unfilterFrame, }; } diff --git a/packages/client/src/hooks/useFrameHeaders.tsx b/packages/client/src/hooks/useFrameHeaders.tsx new file mode 100644 index 0000000000..ecf299cef4 --- /dev/null +++ b/packages/client/src/hooks/useFrameHeaders.tsx @@ -0,0 +1,81 @@ +import { useContext, useEffect } from 'react'; +import { BlocksContext } from '@/contexts'; + +import { usePixel } from './usePixel'; + +/** + * Use a frame's header's in an insight + * + * @param frame + * @param options + */ +export function useFrameHeaders( + /** Frame to get data from */ + frame = '', + + /** Options for the frame */ + options: Partial<{ + /** Selector to grab the data */ + headerTypes: string[]; + }> = {}, +) { + const { headerTypes = [] } = options; + + const context = useContext(BlocksContext); + if (context === undefined) { + throw new Error('useFrameHeaders must be used within Blocks'); + } + + const { state } = context; + + // get the frameKey, this will change whenever the data does + const frameKey = state.getFrameKey(frame); + + /** + * Get the headers + */ + const getHeaders = usePixel<{ + name: string; + type: string; + headerInfo: { + headers: { + alias: string; + header: string; + dataType: string; + adtlType: string; + qsName: unknown; + }[]; + joins: unknown[]; + }; + }>( + frame + ? `META | ${frame} | FrameHeaders(${ + headerTypes && headerTypes.length !== 0 + ? `headerTypes=${JSON.stringify(headerTypes)}` + : '' + });` + : '', + { + silent: true, + }, + context.state.insightId, + ); + + // refresh the data whenever the key changes + useEffect(() => { + getHeaders.refresh(); + }, [frameKey]); + + return { + isLoading: getHeaders.status === 'LOADING', + data: + getHeaders.status === 'SUCCESS' + ? { + list: getHeaders.data.headerInfo.headers, + } + : { + list: [], + }, + error: getHeaders.error, + }; +} diff --git a/packages/client/src/stores/state/cell.state.ts b/packages/client/src/stores/state/cell.state.ts index 5a56c6ab0c..6f29625f56 100644 --- a/packages/client/src/stores/state/cell.state.ts +++ b/packages/client/src/stores/state/cell.state.ts @@ -5,7 +5,7 @@ import { setValueByPath } from '@/utility'; import { CellComponent, CellConfig, CellDef } from './state.types'; import { StateStore } from './state.store'; import { QueryState } from './query.state'; -import { pixelConsole, pixelResult, runPixelAsync, download } from '@/api'; +import { pixelConsole, pixelResult, runPixelAsync } from '@/api'; export interface CellStateStoreInterface { /** Id of the cell */ @@ -249,82 +249,10 @@ export class CellState { * Helpers */ /** - * Process State - */ - /** - * Process running of the cell - */ - async _processRun() { - const start = new Date(); - - try { - // check the loading state - if (this._store.isLoading) { - throw new Error('Cell is loading'); - } - - // start the loading screen - this._store.isLoading = true; - - // convert the cells to the raw pixel - const raw: string | string[] = this.toPixel(); - - // Determine if multiple pixels need to be ran. - if (typeof raw === 'string') { - const { opType, output } = await this.runPixel(raw); - - runInAction(() => { - // store the operation and output - this._store.operation = opType; - - // save the last output - this._store.output = output; - }); - } else if (Array.isArray(raw)) { - // Collect responses for each call to store in state. - let opTypes = []; - const outputs = []; - - for (const str of raw) { - const { opType, output } = await this.runPixel(str); - opTypes = [...opTypes, ...opType]; - outputs.push(output); - } - - runInAction(() => { - // store the operation and output - this._store.operation = opTypes; - - // save the last output - this._store.output = outputs; - }); - } - } catch (e) { - runInAction(() => { - // store the operation and output - this._store.operation = ['ERROR']; - - // save the last output - this._store.output = e.message; - }); - } finally { - const end = new Date(); - - this._store.executionDurationMilliseconds = - end.getTime() - start.getTime(); - - runInAction(() => { - // stop the loading screen - this._store.isLoading = false; - }); - } - } - - /** - * Helper function for _processRun + * Helper function to run a pixel * @param rawPixel - pixel to be formatted and run */ - async runPixel(rawPixel: string) { + private async runPixel(rawPixel: string) { // Gets rid of braces and evaluate parameters in query // const filled = this._state.flattenVar(raw); const filled = this._state.flattenVariable(rawPixel); @@ -369,7 +297,6 @@ export class CellState { } const { errors, results } = await pixelResult(jobId); - if (errors.length > 0) { throw new Error(errors.join('')); } @@ -400,19 +327,91 @@ export class CellState { output = last.output; } - if (opType.includes('FILE_DOWNLOAD')) { - await download(this._state.insightId, output as string); - } - return { opType, output }; } + /** + * Run the cell + */ + async _run() { + const start = new Date(); + + try { + // check the loading state + if (this._store.isLoading) { + throw new Error('Cell is already loading'); + } + + // start the loading screen + this._store.isLoading = true; + + // convert the cells to the raw pixel + const raw: string | string[] = this.toPixel(); + + // Determine if multiple pixels need to be ran. + if (typeof raw === 'string') { + const { opType, output } = await this.runPixel(raw); + + runInAction(() => { + // store the operation and output + this._store.operation = opType; + + // save the last output + this._store.output = output; + }); + } else if (Array.isArray(raw)) { + // Collect responses for each call to store in state. + let opTypes = []; + const outputs = []; + + for (const str of raw) { + const { opType, output } = await this.runPixel(str); + opTypes = [...opTypes, ...opType]; + outputs.push(output); + } + + runInAction(() => { + // store the operation and output + this._store.operation = opTypes; + + // save the last output + this._store.output = outputs; + }); + } + + // log it + console.log(JSON.stringify(this.operation), this.output); + + // process side effects from running a pixel + this._state.processSideEffects(this.operation, this.output); + } catch (e) { + runInAction(() => { + // store the operation and output + this._store.operation = ['ERROR']; + + // save the last output + this._store.output = e.message; + }); + } finally { + runInAction(() => { + //TODO: Integrate with backend + const end = new Date(); + + this._store.executionDurationMilliseconds = + end.getTime() - start.getTime(); + + // stop the loading screen + this._store.isLoading = false; + }); + } + } + /** * Update the the store of the cell * @param path - path of the data to set * @param value - value of the data */ - _processUpdate(path: string | null, value: unknown) { + _update(path: string | null, value: unknown) { if (!path) { // set the value this._store = value as CellStateStoreInterface; diff --git a/packages/client/src/stores/state/query.state.ts b/packages/client/src/stores/state/query.state.ts index 0e4f0207c5..f5a0bb52ff 100644 --- a/packages/client/src/stores/state/query.state.ts +++ b/packages/client/src/stores/state/query.state.ts @@ -198,6 +198,13 @@ export class QueryState { return this._store.cells; } + /** + * Get the cells of the query as a list + */ + get cellList() { + return this._store.list.map((cId) => this._store.cells[cId]); + } + /** * Get a cell from the query * @param id - id of the cell to get @@ -232,9 +239,9 @@ export class QueryState { * Helpers */ /** - * Process running of a pixel + * Run the query */ - _processRun = async () => { + _run = async () => { try { // check the loading state if (this._store.isLoading) { @@ -249,7 +256,7 @@ export class QueryState { const cell = this._store.cells[s]; // run the cell - await cell._processRun(); + await cell._run(); } } catch (e) { // if a cell errors out of the runPixel and causes a break/catch here, @@ -270,7 +277,7 @@ export class QueryState { * @param path - path of the data to set * @param value - value of the data */ - _processUpdate = (path: string | null, value: unknown) => { + _update = (path: string | null, value: unknown) => { if (!path) { // set the value this._store = value as QueryStateStoreInterface; @@ -282,11 +289,11 @@ export class QueryState { }; /** - * Process adding a new cell to the query + * Add a cell * @param cell - new cell being added * @param previousCellId - id of the previous cell */ - _processNewCell = ( + _addCell = ( cellId: string, config: Omit, previousCellId: string, @@ -323,10 +330,10 @@ export class QueryState { }; /** - * Process deleting a cell from the query + * Remove a cell * @param id - id of the cell to delete */ - _processDeleteCell = (id: string) => { + _removeCell = (id: string) => { // find the index to delete at const deleteCellIdx = this._store.list.indexOf(id); if (deleteCellIdx === -1) { diff --git a/packages/client/src/stores/state/state.store.ts b/packages/client/src/stores/state/state.store.ts index fd3d6a1f9d..3c4e9e41b5 100644 --- a/packages/client/src/stores/state/state.store.ts +++ b/packages/client/src/stores/state/state.store.ts @@ -1,5 +1,6 @@ -import { makeAutoObservable, toJS } from 'mobx'; +import { makeAutoObservable, runInAction, toJS } from 'mobx'; +import { download, runPixel } from '@/api'; import { cancellablePromise, getValueByPath } from '@/utility'; import { @@ -18,6 +19,7 @@ import { Variable, VariableType, VariableWithId, + Frame, } from './state.types'; import { QueryState, QueryStateConfig } from './query.state'; import { CellStateConfig } from './cell.state'; @@ -39,6 +41,9 @@ interface StateStoreInterface { /** Blocks rendered in the insight */ blocks: Record; + /** Frames stored in the insight */ + frames: Record; + /** Cells registered to the insight */ cellRegistry: CellRegistry; @@ -79,6 +84,7 @@ export class StateStore { version: '', queries: {}, blocks: {}, + frames: {}, cellRegistry: {}, variables: {}, dependencies: {}, // Maher said change to constants @@ -324,6 +330,21 @@ export class StateStore { return alias; } + /** + * Get a frame. Create one if it isn't there + * @param name + */ + getFrameKey(name: string): Frame['key'] { + // create the frame if it is not there + if (!this._store.frames[name]) { + runInAction(() => { + this.createFrame(name); + }); + } + + return this._store.frames[name].key; + } + /** * Actions */ @@ -525,6 +546,42 @@ export class StateStore { }); }; + /** Side effects Methods */ + /** + * Run a side effect pixel and process the response + * + * @param pixel - side effect to run + */ + runSideEffect = async (pixel: string) => { + const response = await runPixel(pixel, this._store.insightId); + + // process the side effects + for (const { operationType, output } of response.pixelReturn) { + this.processSideEffects(operationType, output); + } + + // return the response + return response; + }; + + /** + * Process side-effects from running a pixel + * + * @param operation - operation that was run + * @param output - output fo the operation + */ + processSideEffects = (operation: string[], output: unknown) => { + // download the file + if (operation.includes('FILE_DOWNLOAD')) { + download(this.insightId, output as string); + } else if ( + operation.includes('FRAME_DATA_CHANGE') || + operation.includes('FRAME_FILTER_CHANGE') + ) { + this.syncFrame((output as { name: string }).name); + } + }; + /** * Serialize to JSON */ @@ -542,6 +599,10 @@ export class StateStore { }; } + /** + * + */ + /** * Internal */ @@ -696,6 +757,29 @@ export class StateStore { block.parent = null; }; + /** + * Create a new frame + */ + private createFrame = (name: string) => { + this._store.frames[name] = { + name: name, + key: 0, + }; + }; + + /** + * Resync the frame and change the data key + */ + private syncFrame = (name: string) => { + // create the frame if it is not there + if (!this._store.frames[name]) { + this.createFrame(name); + } + + // increment the key + this._store.frames[name].key = this._store.frames[name].key + 1; + }; + /** * Actions */ @@ -1087,7 +1171,7 @@ export class StateStore { const q = this._store.queries[queryId]; // set the value - q._processUpdate(path, value); + q._update(path, value); }; /** @@ -1105,7 +1189,7 @@ export class StateStore { // setup the promise const p = cancellablePromise(async () => { // run the query - await q._processRun(); + await q._run(); // turn it off return true; @@ -1140,7 +1224,7 @@ export class StateStore { const q = this._store.queries[queryId]; // add the cell - q._processNewCell(cellId, config, previousCellId); + q._addCell(cellId, config, previousCellId); }; /** @@ -1152,8 +1236,8 @@ export class StateStore { // get the query const q = this._store.queries[queryId]; - // add the cell - q._processDeleteCell(cellId); + // remove the cell + q._removeCell(cellId); // clean up variables Object.entries(this._store.variables).forEach((keyValue) => { @@ -1202,7 +1286,7 @@ export class StateStore { const s = q.getCell(cellId); // set the value - s._processUpdate(path, value); + s._update(path, value); }; /** @@ -1212,7 +1296,7 @@ export class StateStore { */ private runCell = (queryId: string, cellId: string): void => { const q = this._store.queries[queryId]; - const s = q.getCell(cellId); + const c = q.getCell(cellId); const key = `cell--${cellId} (query--${queryId});`; @@ -1222,7 +1306,7 @@ export class StateStore { // setup the promise const p = cancellablePromise(async () => { // run the cell - await s._processRun(); + await c._run(); // turn it off return true; diff --git a/packages/client/src/stores/state/state.types.ts b/packages/client/src/stores/state/state.types.ts index 2c8639f1bb..c4096617f2 100644 --- a/packages/client/src/stores/state/state.types.ts +++ b/packages/client/src/stores/state/state.types.ts @@ -78,6 +78,17 @@ export type VariableWithId = isOutput?: boolean; } & { id: string }); +/** + * Frame + */ +export type Frame = { + /** Name of the frame */ + name: string; + + /** Key associated with the frame, it changes whenever the data changes */ + key: number; +}; + /** * Variants */ diff --git a/packages/ui/src/components/List/List.tsx b/packages/ui/src/components/List/List.tsx index cd20d077d5..37bcb981cb 100644 --- a/packages/ui/src/components/List/List.tsx +++ b/packages/ui/src/components/List/List.tsx @@ -1,6 +1,7 @@ -import { List as MuiList, SxProps } from "@mui/material"; +import { ForwardedRef, forwardRef } from "react"; +import { List as MuiList, ListProps as MuiListProps } from "@mui/material"; -export interface ListProps { +export interface ListProps extends MuiListProps { /** * The content of the component. */ @@ -25,18 +26,23 @@ export interface ListProps { */ subheader?: React.ReactNode; - /** - * The system prop that allows defining system overrides as well as additional CSS styles. - */ - sx?: SxProps; - /** * String to use a HTML element for root node */ component?: string; } -export const List = (props: ListProps) => { - const { sx } = props; - return ; +const _List = ( + props: ListProps, + ref: ForwardedRef, +): JSX.Element => { + const { children, ...otherProps } = props; + + return ( + + {children} + + ); }; + +export const List = forwardRef(_List); diff --git a/packages/ui/src/components/List/ListItem.tsx b/packages/ui/src/components/List/ListItem.tsx index 611f0a3084..b055e0034d 100644 --- a/packages/ui/src/components/List/ListItem.tsx +++ b/packages/ui/src/components/List/ListItem.tsx @@ -1,6 +1,10 @@ -import { ListItem as MuiListItem, SxProps } from "@mui/material"; +import { ForwardedRef, forwardRef } from "react"; +import { + ListItem as MuiListItem, + ListItemProps as MuiListItemProps, +} from "@mui/material"; -export interface ListItemProps { +export interface ListItemProps extends MuiListItemProps { /** * Defines the `align-items` style property. * @default 'center' @@ -64,25 +68,19 @@ export interface ListItemProps { * @deprecated checkout [ListItemButton](/material-ui/api/list-item-button/) instead */ selected?: boolean; - - /** - * The system prop that allows defining system overrides as well as additional CSS styles. - */ - sx?: SxProps; - - /** - * Events we need access to for list items - */ - onClick?: React.MouseEventHandler; - onMouseDown?: React.MouseEventHandler; - onHover?: React.MouseEventHandler; - onMouseOver?: React.MouseEventHandler; - onMouseLeave?: React.MouseEventHandler; - onFocus?: React.FocusEventHandler; - onBlur?: React.FocusEventHandler; } -export const ListItem = (props: ListItemProps) => { - const { sx } = props; - return ; +const _ListItem = ( + props: ListItemProps, + ref: ForwardedRef, +): JSX.Element => { + const { children, ...otherProps } = props; + + return ( + + {children} + + ); }; + +export const ListItem = forwardRef(_ListItem); diff --git a/packages/ui/src/components/List/ListItemText.tsx b/packages/ui/src/components/List/ListItemText.tsx index 5d742f2888..d96eeaea0c 100644 --- a/packages/ui/src/components/List/ListItemText.tsx +++ b/packages/ui/src/components/List/ListItemText.tsx @@ -1,27 +1,9 @@ -import { ListItemText as MuiListItemText, SxProps } from "@mui/material"; - -export interface ListItemTextProps { - /** - * Alias for the `primary` prop. - */ - children?: React.ReactNode; - - /** - * If `true`, the children won't be wrapped by a Typography component. - * This can be useful to render an alternative Typography variant by wrapping - * the `children` (or `primary`) text, and optional `secondary` text - * with the Typography component. - * @default false - */ - disableTypography?: boolean; - - /** - * If `true`, the children are indented. - * This should be used if there is no left avatar or left icon. - * @default false - */ - inset?: boolean; +import { + ListItemText as MuiListItemText, + ListItemTextProps as MuiListItemTextProps, +} from "@mui/material"; +export interface ListItemTextProps extends MuiListItemTextProps { /** * The main content element. */ @@ -31,23 +13,7 @@ export interface ListItemTextProps { * The secondary content element. */ secondary?: React.ReactNode; - - /** - * Props to be applied to primary content - */ - primaryTypographyProps?: object; - - /** - * Props to be applied to secondary content - */ - secondaryTypographyProps?: object; - - /** - * The system prop that allows defining system overrides as well as additional CSS styles. - */ - sx?: SxProps; } export const ListItemText = (props: ListItemTextProps) => { - const { sx } = props; - return ; + return ; };