diff --git a/apps/app/src/App.tsx b/apps/app/src/App.tsx index 716f858ee3..8cb847b221 100644 --- a/apps/app/src/App.tsx +++ b/apps/app/src/App.tsx @@ -18,7 +18,7 @@ const App = () => { return ( diff --git a/apps/app/src/components/common/Container/Container.tsx b/apps/app/src/components/common/Container/Container.tsx index 221b89c7d0..539d90adc4 100644 --- a/apps/app/src/components/common/Container/Container.tsx +++ b/apps/app/src/components/common/Container/Container.tsx @@ -33,7 +33,7 @@ export const Container = ({ const spacing = useHeaderSpacing(); return ( -
+
}>{children} diff --git a/apps/app/src/generator/CodeEditor/CodeEditor.tsx b/apps/app/src/generator/CodeEditor/CodeEditor.tsx index 678f359078..6a3ae1e38f 100644 --- a/apps/app/src/generator/CodeEditor/CodeEditor.tsx +++ b/apps/app/src/generator/CodeEditor/CodeEditor.tsx @@ -55,7 +55,7 @@ const CodeEditor = ({ const onResetHandler = () => { updateCustomTheme({}, { isReset: true, updateThemeChanges: false }); - changeTheme("ds5", "dawn"); + changeTheme("pentahoPlus", "dawn"); }; const codeChangedHandler = (code?: string) => { @@ -69,7 +69,7 @@ const CodeEditor = ({ try { const parsed = JSON5.parse(snippet); if (customTheme.base !== parsed.base) { - if (parsed.base === "ds3" || parsed.base === "ds5") { + if (["pentahoPlus", "ds3", "ds5"].includes(parsed.base)) { changeTheme(parsed.base, selectedMode); updateCustomTheme( { ...parsed }, diff --git a/apps/app/src/generator/GeneratorContext.tsx b/apps/app/src/generator/GeneratorContext.tsx index 8e44423322..443ba750f5 100644 --- a/apps/app/src/generator/GeneratorContext.tsx +++ b/apps/app/src/generator/GeneratorContext.tsx @@ -50,7 +50,7 @@ export const GeneratorContext = createContext( null, ); -const initialTheme = createTheme({ name: "customTheme", base: "ds5" }); +const initialTheme = createTheme({ name: "customTheme", base: "pentahoPlus" }); const GeneratorProvider = ({ children }: { children: React.ReactNode }) => { const [open, setOpen] = useState(false); @@ -80,7 +80,7 @@ const GeneratorProvider = ({ children }: { children: React.ReactNode }) => { if (isReset) { setThemeChanges({}); newTheme = createTheme({ - base: "ds5", + base: "pentahoPlus", name: "customTheme", }); return newTheme; diff --git a/apps/app/src/lib/navigation.ts b/apps/app/src/lib/navigation.ts index 0adf544be3..7432778b28 100644 --- a/apps/app/src/lib/navigation.ts +++ b/apps/app/src/lib/navigation.ts @@ -29,6 +29,11 @@ const navigation = [ label: "Kanban Board", path: "/templates/kanban-board", }, + { + id: "canvas", + label: "Canvas", + path: "/templates/canvas", + }, ], }, { id: "flow", label: "Flow", path: "/flow" }, diff --git a/apps/app/src/routes.tsx b/apps/app/src/routes.tsx index 5a89ab4760..4cd8d03380 100644 --- a/apps/app/src/routes.tsx +++ b/apps/app/src/routes.tsx @@ -9,6 +9,7 @@ const DetailsView = lazy(() => import("../../../templates/DetailsView")); const Dashboard = lazy(() => import("../../../templates/Dashboard")); const Welcome = lazy(() => import("../../../templates/Welcome")); const KanbanBoard = lazy(() => import("../../../templates/KanbanBoard")); +const Canvas = lazy(() => import("../../../templates/Canvas")); export const routes: RouteObject[] = [ { @@ -29,6 +30,7 @@ export const routes: RouteObject[] = [ { path: "form", element:
}, { path: "details-view", element: }, { path: "kanban-board", element: }, + { path: "canvas", element: }, ], }, { path: "/*", lazy: () => import("~/pages/NotFound") }, diff --git a/docs/templates/Canvas.stories.tsx b/docs/templates/Canvas.stories.tsx new file mode 100644 index 0000000000..9643801d47 --- /dev/null +++ b/docs/templates/Canvas.stories.tsx @@ -0,0 +1,34 @@ +import { css } from "@emotion/css"; +import { StoryObj } from "@storybook/react"; +import { + canvasPanelClasses, + canvasToolbarClasses, +} from "@hitachivantara/uikit-react-pentaho"; + +import Canvas from "../../templates/Canvas"; +import CanvasRaw from "../../templates/Canvas?raw"; + +export default { + title: "Templates/Canvas", +}; + +const classes = { + // Needed to override the styles specific to the app but can't be used in Storybook + root: css({ + "& > div": { height: "calc(100vh - 40px)" }, + [`& .${canvasToolbarClasses.root}`]: { top: 8 }, + [`& .${canvasPanelClasses.root}`]: { top: 8, height: "calc(100% - 8px)" }, + }), +}; + +export const Main: StoryObj = { + parameters: { + docs: { + source: { + code: CanvasRaw, + }, + }, + }, + decorators: [(Story) =>
{Story()}
], + render: () => , +}; diff --git a/docs/templates/TemplateItem.tsx b/docs/templates/TemplateItem.tsx index 067dc5d8f8..d4b0896456 100644 --- a/docs/templates/TemplateItem.tsx +++ b/docs/templates/TemplateItem.tsx @@ -77,6 +77,11 @@ const templates = [ img: "https://i.imgur.com/QQ0WvmN.png", href: "./?path=/docs/templates-welcome--docs", }, + { + id: "Canvas", + img: "https://i.imgur.com/fViaBkg.png", + href: "./?path=/docs/templates-canvas--docs", + }, ]; export const TemplateItems = () => { diff --git a/packages/cli/src/templates/Canvas/Context.tsx b/packages/cli/src/templates/Canvas/Context.tsx new file mode 100644 index 0000000000..44026cc703 --- /dev/null +++ b/packages/cli/src/templates/Canvas/Context.tsx @@ -0,0 +1,49 @@ +import { + createContext, + Dispatch, + SetStateAction, + useContext, + useMemo, + useState, +} from "react"; + +interface SelectedTable { + id: string; + label: React.ReactNode; +} + +interface CanvasContextValue { + selectedTable: string; + setSelectedTable?: Dispatch>; + openedTables?: SelectedTable[]; + setOpenedTables?: Dispatch>; +} + +const CanvasContext = createContext({ + selectedTable: "none", +}); + +interface CanvasProviderProps { + children?: React.ReactNode; +} + +export const CanvasProvider = ({ children }: CanvasProviderProps) => { + const [openedTables, setOpenedTables] = useState(); + const [selectedTable, setSelectedTable] = useState("none"); + + const value = useMemo( + () => ({ + openedTables, + setOpenedTables, + selectedTable, + setSelectedTable, + }), + [openedTables, selectedTable], + ); + + return ( + {children} + ); +}; + +export const useCanvasContext = () => useContext(CanvasContext); diff --git a/packages/cli/src/templates/Canvas/ListView.tsx b/packages/cli/src/templates/Canvas/ListView.tsx new file mode 100644 index 0000000000..1776dd39ed --- /dev/null +++ b/packages/cli/src/templates/Canvas/ListView.tsx @@ -0,0 +1,189 @@ +import { forwardRef, useRef, useState } from "react"; +import { useDraggable } from "@dnd-kit/core"; +import { css, cx } from "@emotion/css"; +import { + HvInput, + HvInputProps, + HvListContainer, + HvListItem, + HvListItemProps, + HvTypography, + outlineStyles, + theme, + useForkRef, + useUniqueId, +} from "@hitachivantara/uikit-react-core"; +import { Drag } from "@hitachivantara/uikit-react-icons"; + +import { NodeData } from "./Node"; +import { iconsMapping, iconsMappingKeys } from "./utils"; + +const classes = { + root: css({ display: "flex", flexDirection: "column", gap: theme.space.sm }), + item: css({ + display: "flex", + justifyContent: "space-between", + alignItems: "center", + border: `1px solid ${theme.colors.atmo3}`, + borderRadius: "12px", + backgroundColor: theme.colors.atmo1, + padding: theme.space.xs, + height: "unset", + "&:focus-visible": { + ...outlineStyles, + }, + }), + itemTitle: css({ + display: "flex", + alignItems: "center", + gap: theme.space.xs, + }), + icon: css({ + borderRadius: theme.radii.round, + backgroundColor: theme.colors.cat6_60, + }), + dragging: css({ + border: `2px solid ${theme.colors.primary_80}`, + }), +}; + +const items = iconsMappingKeys.map((key, index) => ({ + id: `item${index + 1}`, + title: `Item ${index + 1}`, + subtitle: `Description ${index + 1}`, + icon: key, +})); + +interface ItemProps { + id: string; + title: string; + subtitle: string; + icon: string; +} + +interface ItemCardProps + extends Omit, + Omit { + isDragging?: boolean; +} + +const ItemCard = forwardRef( + ({ icon, title, subtitle, isDragging, ...others }, ref) => { + return ( + +
+
{iconsMapping[icon]}
+ {title} +
+ +
+ ); + }, +); + +const DraggableItemCard = ({ icon, title, id, subtitle }: ItemProps) => { + const itemRef = useRef(null); + + const { attributes, listeners, setNodeRef, isDragging, transform } = + useDraggable({ + id, + data: { + // Data needed to be dropped in HvFlow + hvFlow: { + // HvFlow will use this value to populate the node's data.nodeLabel + label: title, + // Node type from nodeTypes property provided to HvFlow + type: "node", + // Item position: used by HvFlow to position the node when dropped + x: itemRef.current?.getBoundingClientRect().x, + y: itemRef.current?.getBoundingClientRect().y, + // Values to be added to the node's data + data: { + subtitle, + color: "cat6_60", + icon, + output: { + id: "data", + label: "Data", + }, + input: { + id: "data", + label: "Data", + }, + } satisfies NodeData, + }, + // Data needed for the DragOverlay component + dragOverlay: { + component: ( + + ), + }, + }, + }); + + const style = transform + ? { + transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`, + } + : undefined; + + const forkedRef = useForkRef(itemRef, setNodeRef); + + return ( + + ); +}; + +export const ListView = () => { + const [listItems, setListItems] = useState(items); + + const listId = useUniqueId(); + + const handleSearch: HvInputProps["onChange"] = (event, value) => { + if (value) { + setListItems( + items.filter((item) => + item.title.toLowerCase().trim().includes(value.toLowerCase().trim()), + ), + ); + } else { + setListItems(items); + } + }; + + return ( +
+ + + {listItems.map((item) => ( + + ))} + +
+ ); +}; diff --git a/packages/cli/src/templates/Canvas/Node.tsx b/packages/cli/src/templates/Canvas/Node.tsx new file mode 100644 index 0000000000..ba22b7aa28 --- /dev/null +++ b/packages/cli/src/templates/Canvas/Node.tsx @@ -0,0 +1,199 @@ +import { isValidElement } from "react"; +import { css, cx } from "@emotion/css"; +import { Handle, NodeProps, NodeToolbar, Position } from "reactflow"; +import { + HvButton, + HvIconButton, + HvTooltip, + HvTypography, +} from "@hitachivantara/uikit-react-core"; +import { + HvFlowNodeInput, + HvFlowNodeOutput, + useHvNode, +} from "@hitachivantara/uikit-react-lab"; +import { HvColorAny, theme } from "@hitachivantara/uikit-styles"; + +import { useCanvasContext } from "./Context"; +import { FlowStatus, flowStatusesSpecs, iconsMapping } from "./utils"; + +const classes = { + root: css({ + display: "flex", + alignItems: "center", + justifyContent: "center", + borderRadius: "16px", + backgroundColor: theme.colors.atmo1, + boxShadow: theme.colors.shadow, + borderWidth: "1px", + minWidth: "200px", + minHeight: "100px", + borderColor: "var(--node-border-color)", + }), + content: css({ + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: theme.spacing("sm"), + gap: theme.space.xs, + }), + contentIcon: css({ + width: 48, + height: 48, + display: "flex", + justifyContent: "center", + alignItems: "center", + borderRadius: theme.radii.round, + backgroundColor: "var(--icon-bg-color)", + }), + nodeToolbar: css({ + backgroundColor: theme.colors.atmo1, + borderRadius: theme.radii.full, + }), + handle: css({ + backgroundColor: theme.colors.secondary_80, + border: `1px solid ${theme.colors.atmo1}`, + height: 8, + width: 8, + }), + statusIcon: css({ position: "absolute", top: -8, right: -8 }), + contentInfo: css({ + display: "flex", + flexDirection: "column", + alignItems: "flex-start", + justifyContent: "center", + "& > button": { marginTop: theme.space.xs, alignSelf: "center" }, + }), +}; + +export type NodeData = + | undefined + | { + nodeLabel?: string; // nodeLabel is automatically added by HvFlow when the node is dropped + tableId?: string; + subtitle?: string; + color?: HvColorAny; + icon?: string; + output?: HvFlowNodeOutput; + input?: HvFlowNodeInput; + status?: FlowStatus; + }; + +export const Node = ({ id, data = {} }: NodeProps) => { + const { + nodeLabel: titleProp, + subtitle: subtitleProp, + color: colorProp, + icon: iconProp, + input: inputProp, + output: outputProp, + status: statusProp, + tableId, + } = data; + + const { + toggleShowActions, + getNodeToolbarProps, + handleDefaultAction, + nodeActions, + color, + subtitle, + icon, + title, + } = useHvNode({ + id, + title: titleProp, + subtitle: subtitleProp, + color: colorProp, + icon: iconProp ? iconsMapping[iconProp] : undefined, + inputs: inputProp ? [inputProp] : undefined, + outputs: outputProp ? [outputProp] : undefined, + }); + + const { selectedTable, setOpenedTables, setSelectedTable } = + useCanvasContext(); + + const status = statusProp ? flowStatusesSpecs[statusProp] : undefined; + + return ( +
+ + {nodeActions?.map((action) => ( + handleDefaultAction(action)} + > + {isValidElement(action.icon) ? action.icon : null} + + ))} + + {inputProp && ( + + )} + {outputProp && ( + + )} +
+
{icon}
+
+ + {title} + + {subtitle} + {tableId && ( + + setOpenedTables?.((prev) => { + const tables = prev ? [...prev] : []; + if (!tables.find((x) => x.id === tableId)) { + if (tables.length === 0 && selectedTable === "none") { + setSelectedTable?.(tableId); + } + tables.push({ + id: tableId, + label: title, + }); + return tables; + } + return prev; + }) + } + > + View Data + + )} +
+
+ {status && ( + + {status.icon} + + )} +
+ ); +}; diff --git a/packages/cli/src/templates/Canvas/Sidebar.tsx b/packages/cli/src/templates/Canvas/Sidebar.tsx new file mode 100644 index 0000000000..b2055d3b32 --- /dev/null +++ b/packages/cli/src/templates/Canvas/Sidebar.tsx @@ -0,0 +1,63 @@ +import { useState } from "react"; +import { + DndContextProps, + DragOverlay, + useDndMonitor, + useDroppable, +} from "@dnd-kit/core"; +import { restrictToWindowEdges } from "@dnd-kit/modifiers"; +import { useTheme, useUniqueId } from "@hitachivantara/uikit-react-core"; +import { + HvCanvasPanel, + HvCanvasPanelProps, +} from "@hitachivantara/uikit-react-pentaho"; + +import { classes } from "./styles"; +import { restrictToSample } from "./utils"; + +export const CanvasSidebar = (props: HvCanvasPanelProps) => { + const { rootId } = useTheme(); + + const [overlay, setOverlay] = useState(); + + const elementId = useUniqueId("canvas-panel"); + + // The sidebar is droppable to distinguish between the canvas and the sidebar + // Otherwise items dropped inside the sidebar will be added to the canvas + const { setNodeRef } = useDroppable({ id: elementId }); + + const handleDragStart: DndContextProps["onDragStart"] = (event) => { + if (event.active.data.current?.dragOverlay) { + setOverlay(event.active.data.current.dragOverlay?.component); + } + }; + + const handleDragEnd: DndContextProps["onDragEnd"] = () => { + setOverlay(undefined); + }; + + useDndMonitor({ + onDragEnd: handleDragEnd, + onDragStart: handleDragStart, + }); + + return ( + <> + + {/** Shown when the dragged item leaves the sidebar to drop it in the canvas */} + restrictToSample(rootId || "", args), // This modifier shouldn't be used in a real use case. It's only needed for Storybook samples. + ]} + > + {overlay ?? null} + + + ); +}; diff --git a/packages/cli/src/templates/Canvas/StatusEdge.tsx b/packages/cli/src/templates/Canvas/StatusEdge.tsx new file mode 100644 index 0000000000..5cab0ec93f --- /dev/null +++ b/packages/cli/src/templates/Canvas/StatusEdge.tsx @@ -0,0 +1,85 @@ +import { css } from "@emotion/css"; +import { + BaseEdge, + EdgeLabelRenderer, + EdgeProps, + getBezierPath, +} from "reactflow"; +import { + HvDropDownMenu, + HvDropDownMenuProps, +} from "@hitachivantara/uikit-react-core"; +import { useFlowInstance } from "@hitachivantara/uikit-react-lab"; + +import { FlowStatus, flowStatusesSpecs } from "./utils"; + +const classes = { + dropdownMenu: css({ + "& svg .color0": { + fill: "var(--color-0)", + }, + }), +}; + +export type StatusEdgeData = + | undefined + | { + status?: FlowStatus; + }; + +export const StatusEdge = (props: EdgeProps) => { + const { + id, + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + data, + } = props; + + const instance = useFlowInstance(); + + const [edgePath, labelX, labelY] = getBezierPath({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + }); + + const status = data?.status ? flowStatusesSpecs[data.status] : undefined; + + const handleClick: HvDropDownMenuProps["onClick"] = (event, value) => { + if (value.id === "remove") { + instance.setEdges((edges) => edges.filter((edge) => edge.id !== id)); + } + }; + + return ( + <> + + {status && ( + +
+ +
+
+ )} + + ); +}; diff --git a/packages/cli/src/templates/Canvas/Table.tsx b/packages/cli/src/templates/Canvas/Table.tsx new file mode 100644 index 0000000000..c2af1b4ef4 --- /dev/null +++ b/packages/cli/src/templates/Canvas/Table.tsx @@ -0,0 +1,202 @@ +import { useState } from "react"; +import useSWR from "swr"; +import { + HvLoadingContainer, + HvPagination, + HvTable, + HvTableBody, + HvTableCell, + HvTableColumnConfig, + HvTableContainer, + HvTableHead, + HvTableHeader, + HvTableRow, + HvTableSection, + HvTypography, + useHvData, + useHvPagination, + useHvSortBy, +} from "@hitachivantara/uikit-react-core"; + +import { delay } from "./utils"; + +const EmptyRow = () => ( + + + +); + +interface Data { + column1: string; + column2: string; + column3: string; + column4: string; + column5: string; + column6: string; +} + +const columns: HvTableColumnConfig[] = [ + { Header: "Column 1", accessor: "column1" }, + { Header: "Column 2", accessor: "column2" }, + { Header: "Column 3", accessor: "column3" }, + { Header: "Column 4", accessor: "column4" }, + { Header: "Column 5", accessor: "column5" }, + { Header: "Column 6", accessor: "column6" }, +]; + +const randomData = [ + "potato", + "carrot", + "apple", + "grape", + "banana", + "orange", + "pineapple", + "blueberry", + "tomato", + "avocado", + "peach", + "watermelon", + "cherry", + "raspberry", +]; + +// Simulating data fetching in backend +const fetchData = async ( + limit: number, +): Promise<{ data: Data[]; total: number; pageCount: number }> => { + await delay(600); + const total = 30; + return { + data: Array.from({ length: limit }).map(() => { + const random = Math.floor(Math.random() * randomData.length); + return { + column1: randomData[random], + column2: randomData[random], + column3: randomData[random], + column4: randomData[random], + column5: randomData[random], + column6: randomData[random], + }; + }), + total, + pageCount: Math.ceil(total / limit), + }; +}; + +const useServerData = (id: string, page = 0, skip = 0, limit = 5) => { + return useSWR( + `/data/${id}?page=${page}&skip=${skip}&limit=${limit}`, + async () => fetchData(limit), + ); +}; + +interface DataTableProps { + id: string; +} + +export const DataTable = ({ id }: DataTableProps) => { + const [pageSize, setPageSize] = useState(5); + const [page, setPage] = useState(0); + + const { data, isLoading } = useServerData( + id, + page, + page * pageSize, + pageSize, + ); + + const { + getTableProps, + getTableBodyProps, + getHvPaginationProps, + prepareRow, + headerGroups, + page: pageData, + } = useHvData( + { + columns, + data: data?.data, + manualPagination: true, + initialState: { pageSize, pageIndex: page }, + pageCount: data?.pageCount, + stateReducer: (newState, action) => { + switch (action.type) { + // Triggers a data fetch + case "init": + case "gotoPage": + case "setPageSize": + setPage(newState.pageIndex ?? 0); + setPageSize(newState.pageSize ?? 5); + break; + default: + break; + } + return newState; + }, + }, + useHvSortBy, + useHvPagination, + ); + + const renderTableRow = (i: number) => { + const row = pageData[i]; + + if (!row) return ; + + prepareRow(row); + + return ( + + {row.cells.map((cell) => ( + + {cell.render("Cell")} + + ))} + + ); + }; + + return ( + Preview Data} + > + + {pageData.length > 0 ? ( + + ) : undefined} + + ); +}; diff --git a/packages/cli/src/templates/Canvas/TreeView.tsx b/packages/cli/src/templates/Canvas/TreeView.tsx new file mode 100644 index 0000000000..14ae1b7bd5 --- /dev/null +++ b/packages/cli/src/templates/Canvas/TreeView.tsx @@ -0,0 +1,178 @@ +import { forwardRef, useRef } from "react"; +import { useDraggable } from "@dnd-kit/core"; +import { css, cx } from "@emotion/css"; +import { + HvOverflowTooltip, + HvTreeItem, + HvTreeItemProps, + HvTreeView, + theme, + useForkRef, +} from "@hitachivantara/uikit-react-core"; +import { DataSource, Drag, Table } from "@hitachivantara/uikit-react-icons"; + +import { NodeData } from "./Node"; + +const classes = { + dragging: css({ + border: `2px solid ${theme.colors.primary_80}`, + }), + contentDragging: css({ + "&&:hover": { + backgroundColor: theme.colors.atmo1, + }, + }), + labelRoot: css({ + display: "flex", + alignItems: "center", + width: "100%", + gap: theme.space.xs, + }), + dragIcon: css({ flex: 1, display: "flex", justifyContent: "flex-end" }), +}; + +interface Data { + id: string; + label: string; + subtitle?: string; + children?: Data[]; +} + +interface TreeItemProps extends HvTreeItemProps { + isDragging?: boolean; +} + +interface DraggableTreeItemProps extends HvTreeItemProps { + subtitle?: string; +} + +const data = [ + { + id: "db1", + label: "Database 1", + children: [ + { id: "table1", label: "Table 1", subtitle: "Table from Database 1" }, + { id: "table2", label: "Table 2", subtitle: "Table from Database 1" }, + { id: "table3", label: "Table 3", subtitle: "Table from Database 1" }, + ], + }, + { + id: "db2", + label: "Database 2", + children: [ + { id: "table4", label: "Table 4", subtitle: "Table from Database 2" }, + { id: "table5", label: "Table 5", subtitle: "Table from Database 2" }, + { id: "table6", label: "Table 6", subtitle: "Table from Database 2" }, + ], + }, +] satisfies Data[]; + +export const TreeItem = forwardRef( + (props, ref) => { + const { className, isDragging, children, nodeId, label, ...others } = props; + const Icon = children ? DataSource : Table; + + return ( + + + + {!children && ( + + + + )} +
+ } + {...others} + > + {children} + + ); + }, +); + +const DraggableTreeItem = (props: DraggableTreeItemProps) => { + const { subtitle, label, children, nodeId, ...others } = props; + + const itemRef = useRef(null); + + const { attributes, listeners, setNodeRef, isDragging, transform } = + useDraggable({ + id: nodeId, + data: { + // Data needed to be dropped in HvFlow + hvFlow: { + // HvFlow will use this value to populate the node's data.nodeLabel + label, + // Node type from nodeTypes property provided to HvFlow + type: "node", + // Item position: used by HvFlow to position the node when dropped + x: itemRef.current?.getBoundingClientRect().x, + y: itemRef.current?.getBoundingClientRect().y, + // Values to be added to the node's data + data: { + tableId: nodeId, + subtitle, + color: "cat3_40", + icon: "table", + output: { + id: "data", + label: "Data", + }, + } satisfies NodeData, + }, + // Data needed for the DragOverlay component + dragOverlay: { + component: , + }, + }, + }); + + const style = transform + ? { + transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`, + } + : undefined; + + const forkedRef = useForkRef(itemRef, setNodeRef); + + return ( + + {children} + + ); +}; + +const renderTreeItem = ({ id, label, children, subtitle }: Data) => + children ? ( + + {children?.map(renderTreeItem)} + + ) : ( + + {children} + + ); + +export const TreeView = () => ( + + {data.map((db) => renderTreeItem(db))} + +); diff --git a/packages/cli/src/templates/Canvas/index.tsx b/packages/cli/src/templates/Canvas/index.tsx new file mode 100644 index 0000000000..9f4c8ecc78 --- /dev/null +++ b/packages/cli/src/templates/Canvas/index.tsx @@ -0,0 +1,301 @@ +import { useMemo, useState } from "react"; +import { cx } from "@emotion/css"; +import { ReactFlowInstance } from "reactflow"; +import { + HvButton, + HvDialog, + HvDialogContent, + HvDialogTitle, + HvInlineEditor, + HvOverflowTooltip, + HvTypography, +} from "@hitachivantara/uikit-react-core"; +import { + Add, + Close, + DataSource, + DropUpXS, + Fullscreen, + Table, +} from "@hitachivantara/uikit-react-icons"; +import { + HvFlow, + HvFlowBackground, + HvFlowControls, + HvFlowEmpty, +} from "@hitachivantara/uikit-react-lab"; +import { + HvCanvasFloatingPanel, + HvCanvasFloatingPanelProps, + HvCanvasToolbar, +} from "@hitachivantara/uikit-react-pentaho"; + +import { CanvasProvider, useCanvasContext } from "./Context"; +import { ListView } from "./ListView"; +import { Node, NodeData } from "./Node"; +import { CanvasSidebar } from "./Sidebar"; +import { StatusEdge } from "./StatusEdge"; +import { classes } from "./styles"; +import { DataTable } from "./Table"; +import { TreeView } from "./TreeView"; +import { flowStatuses } from "./utils"; + +const nodeTypes = { + node: Node, +}; +const edgeTypes = { + status: StatusEdge, +}; +const initialNodes = []; +const initialEdges = []; + +const sidePanelTabs = [ + { + id: 0, + content: ( +
+ + Data +
+ ), + }, + { + id: 2, + content: ( +
+ + Nodes +
+ ), + }, +]; +const sidePanelContent = { + [sidePanelTabs[0].id]: , + [sidePanelTabs[1].id]: , +}; + +const Page = () => { + const [sidePanelOpen, setSidePanelOpen] = useState(false); + const [sidePanelTab, setSidePanelTab] = useState(sidePanelTabs[0].id); + const [minimize, setMinimize] = useState(false); + const [fullscreen, setFullscreen] = useState(false); + const [flowInstance, setFlowInstance] = + useState>(); + + const { selectedTable, openedTables, setOpenedTables, setSelectedTable } = + useCanvasContext(); + + const floatingTabs = useMemo( + () => + openedTables?.map((table) => ({ + id: table.id, + title: ( +
+ +
+ +
+ + ), + })), + [openedTables], + ); + + const handleCloseTab = (value: string | number) => { + const newOpenedTables = openedTables?.filter((x) => x.id !== value) ?? []; + if (newOpenedTables.length !== 0) { + setOpenedTables?.(newOpenedTables); + setSelectedTable?.(newOpenedTables[0].id as string); + } else { + setOpenedTables?.(undefined); + } + }; + + const handleChangeTab: HvCanvasFloatingPanelProps["onTabChange"] = ( + event, + value, + ) => { + setSelectedTable?.(value as string); + }; + + const handleAction: HvCanvasFloatingPanelProps["onAction"] = ( + event, + action, + tabId, + ) => { + switch (action.id) { + case "close": + event.stopPropagation(); + handleCloseTab(tabId); + break; + case "toggle": + if (minimize && selectedTable !== tabId) handleChangeTab(null, tabId); + setMinimize((prev) => !prev); + break; + case "fullscreen": + if (minimize && selectedTable !== tabId) handleChangeTab(null, tabId); + setFullscreen((prev) => !prev); + break; + default: + break; + } + }; + + // Simulating an execution of the flow and updating the statuses of nodes and edges + const handleExecute = () => { + flowInstance?.setNodes((nodes) => + nodes.map((node) => { + const random = Math.floor(Math.random() * flowStatuses.length); + return { + ...node, + data: { + ...node.data, + status: flowStatuses[random], + }, + }; + }), + ); + flowInstance?.setEdges((edges) => + edges.map((edge) => { + const random = Math.floor(Math.random() * flowStatuses.length); + return { + ...edge, + type: Object.keys(edgeTypes)[0], + data: { + ...edge.data, + status: flowStatuses[random], + }, + }; + }), + ); + }; + + const floatingPanelOpen = useMemo( + () => + !!openedTables && + openedTables.length > 0 && + floatingTabs && + floatingTabs.length > 0, + [floatingTabs, openedTables], + ); + + return ( +
+ setSidePanelOpen(value)} + onTabChange={(event, value) => setSidePanelTab(value as number)} + > + {sidePanelContent[sidePanelTab]} + + } + > + + Drag and Drop your Nodes + + } + message={ + + Then you can start configuring your flow. + + } + icon={null} + /> + + + + } + > + + Execute + + + {floatingTabs && floatingPanelOpen && ( + + ), + }, + ]} + rightActions={[ + { + id: "fullscreen", + label: "Fullscreen", + icon: , + }, + { + id: "close", + label: "Close", + icon: , + }, + ]} + onTabChange={handleChangeTab} + onAction={handleAction} + > + + + )} + {floatingPanelOpen && ( + setFullscreen((prev) => !prev)} + > + + {floatingTabs?.find((x) => x.id === selectedTable)?.title} + + + + + + )} +
+ ); +}; + +const Canvas = () => ( + + + +); + +export default Canvas; diff --git a/packages/cli/src/templates/Canvas/styles.tsx b/packages/cli/src/templates/Canvas/styles.tsx new file mode 100644 index 0000000000..943e2c8bea --- /dev/null +++ b/packages/cli/src/templates/Canvas/styles.tsx @@ -0,0 +1,54 @@ +import { css } from "@emotion/css"; +import { theme } from "@hitachivantara/uikit-react-core"; + +export const classes = { + flow: css({ + width: "100%", + height: "100%", + // border style when a node is selected + [`& .selected > div`]: { + borderWidth: "2px", + borderColor: theme.colors.primary_80, + }, + }), + flowEmpty: css({ backgroundColor: "transparent" }), + flowEmptyMessage: css({ color: theme.colors.secondary_80 }), + tabLabel: css({ display: "flex", alignItems: "center" }), + root: css({ + height: "100%", + display: "flex", + width: "100%", + }), + toolbar: css({ + top: `calc(${theme.header.height} + ${theme.header.secondLevelHeight} + ${theme.space.md})`, + }), + fullWidth: css({ + left: 0, + right: 0, + marginLeft: "auto", + marginRight: "auto", + width: `calc(100% - 2 * ${theme.space.md})`, + }), + minWidth: css({ + right: theme.space.md, + width: `calc(100% - 320px - 3 * ${theme.space.md})`, + }), + panel: css({ + top: `calc(${theme.header.height} + ${theme.header.secondLevelHeight})`, + height: `calc(100% - ${theme.header.height} - ${theme.header.secondLevelHeight})`, + }), + toggleIcon: css({ transition: "rotate 0.2s ease" }), + titleContainer: css({ + display: "flex", + width: "100%", + }), + titleRoot: css({ + display: "flex", + width: "100%", + alignItems: "center", + }), + dialogTitle: css({ + ...theme.typography.label, + "& div > div": { margin: 0, padding: 0 }, + }), +}; diff --git a/packages/cli/src/templates/Canvas/utils.tsx b/packages/cli/src/templates/Canvas/utils.tsx new file mode 100644 index 0000000000..598da64be6 --- /dev/null +++ b/packages/cli/src/templates/Canvas/utils.tsx @@ -0,0 +1,95 @@ +import { Modifier } from "@dnd-kit/core"; +import { theme } from "@hitachivantara/uikit-react-core"; +import { + Battery, + Cloud, + Edit, + Favorite, + Fire, + Ghost, + Heart, + Level0Good, + Level2Average, + Level3Bad, + Palette, + Table, +} from "@hitachivantara/uikit-react-icons"; + +export const delay = (ms: number) => + new Promise((resolve) => { + setTimeout(resolve, ms); + }); + +export const iconsMapping = { + table:
, + ghost: , + cloud: , + battery: , + fire: , + palette: , + edit: , + heart: , + favorite: , +}; + +export const iconsMappingKeys = Object.keys(iconsMapping); + +const iconWrapper = (icon: React.ReactNode) => ( +
+ {icon} +
+); + +export const flowStatuses = ["warning", "error", "success"] as const; +export type FlowStatus = (typeof flowStatuses)[number]; +export const flowStatusesSpecs = { + [flowStatuses[0]]: { + icon: iconWrapper(), + color: theme.colors.warning, + description: "Warning", + }, + [flowStatuses[1]]: { + icon: iconWrapper(), + color: theme.colors.negative_80, + description: "Error", + }, + [flowStatuses[2]]: { + icon: iconWrapper(), + color: theme.colors.positive, + description: "Success", + }, +}; + +// --- ONLY FOR STORYBOOK, NOT REAL USE CASES --- + +// Fixes a problem we have while dragging node types from the sidebar to the flow in storybook docs mode +type RestrictToSampleModifier = Modifier extends (...args: infer A) => infer R + ? (rootId: string, ...args: A) => R + : unknown; + +// This is only needed for Storybook +// Real use cases shouldn't use this modifier +export const restrictToSample: RestrictToSampleModifier = ( + rootId, + { transform }, +) => { + const rect = document.getElementById(rootId)?.getBoundingClientRect(); + + const docsMode = window.location.search.includes("?viewMode=docs"); + + return { + ...transform, + x: docsMode && rect?.x ? -rect.x + transform.x : transform.x, + y: docsMode && rect?.y ? -rect.y + transform.y : transform.y, + }; +}; diff --git a/packages/lab/src/Flow/Background/Background.tsx b/packages/lab/src/Flow/Background/Background.tsx index 9d2328ed91..b885caf8b5 100644 --- a/packages/lab/src/Flow/Background/Background.tsx +++ b/packages/lab/src/Flow/Background/Background.tsx @@ -11,6 +11,10 @@ export const HvFlowBackground = ({ ...others }: HvFlowBackgroundProps) => { return ( - + ); }; diff --git a/packages/lab/src/Flow/Node/BaseNode.styles.tsx b/packages/lab/src/Flow/Node/BaseNode.styles.tsx index 988d505a80..af71a70e5f 100644 --- a/packages/lab/src/Flow/Node/BaseNode.styles.tsx +++ b/packages/lab/src/Flow/Node/BaseNode.styles.tsx @@ -6,6 +6,7 @@ export const { staticClasses, useClasses } = createClasses("HvFlowBaseNode", { backgroundColor: theme.colors.atmo1, boxShadow: theme.colors.shadow, minWidth: "250px", + border: "1px solid var(--node-color)", }, headerContainer: { padding: theme.spacing(0.5, 1), @@ -15,11 +16,13 @@ export const { staticClasses, useClasses } = createClasses("HvFlowBaseNode", { alignItems: "center", borderTopLeftRadius: "inherit", borderTopRightRadius: "inherit", + backgroundColor: "var(--node-color)", }, titleContainer: { display: "flex", flexDirection: "row", alignItems: "center", + "& svg *.color0": { fill: "var(--icon-color)" }, }, title: { color: theme.colors.base_dark, diff --git a/packages/lab/src/Flow/Node/BaseNode.tsx b/packages/lab/src/Flow/Node/BaseNode.tsx index 4ea0cf26ef..a27bf5a353 100644 --- a/packages/lab/src/Flow/Node/BaseNode.tsx +++ b/packages/lab/src/Flow/Node/BaseNode.tsx @@ -89,7 +89,7 @@ export const HvFlowBaseNode = ({ const labels = useLabels(DEFAULT_LABELS, labelsProp); - const { classes, cx, css } = useClasses(classesProp); + const { classes, cx } = useClasses(classesProp); const renderOutput = (output: HvFlowNodeOutput) => { const edgeConnected = isConnected(id, "source", output.id!, outputEdges); @@ -139,9 +139,12 @@ export const HvFlowBaseNode = ({ return (
))} -
+
{icon} = ({ - id, - title: titleProp, - subtitle: subtitleProp, - groupId = "teapot", - color: colorProp, - icon: iconProp, - inputs: inputsProp, - outputs: outputsProp, -}: HvFlowNodeProps) => { +interface NodeProps extends ReactFlowNodeProps { + groupId: string; + input?: HvFlowNodeInput; + output?: HvFlowNodeOutput; +} + +export const Node = ({ id, groupId = "teapot", input, output }: NodeProps) => { const { toggleShowActions, getNodeToolbarProps, @@ -72,27 +77,22 @@ export const Node: HvFlowNodeFC = ({ icon, color, subtitle, - showActions, - outputEdges, - inputs, - outputs, } = useHvNode({ id, - title: titleProp, - subtitle: subtitleProp, - color: colorProp, - inputs: inputsProp, - outputs: outputsProp, - icon: iconProp, groupId, + inputs: input ? [input] : undefined, + outputs: output ? [output] : undefined, }); + + const edges = useFlowNodeEdges(); + return (
@@ -103,31 +103,29 @@ export const Node: HvFlowNodeFC = ({ title={action.label} onClick={() => handleDefaultAction(action)} > - {renderedIcon(action.icon)} + {action.icon as React.ReactNode} ))} -
- {isConnected(id, "source", "0", outputEdges) ? ( + {edges.length > 0 && ( +
- ) : ( - - )} -
- {inputs && inputs?.length > 0 && ( +
+ )} + {input && ( )} - {outputs && outputs?.length > 0 && ( + {output && ( )}
diff --git a/packages/lab/src/Flow/stories/BaseHook/index.tsx b/packages/lab/src/Flow/stories/BaseHook/index.tsx index 5d4d39e10e..6054c3caba 100644 --- a/packages/lab/src/Flow/stories/BaseHook/index.tsx +++ b/packages/lab/src/Flow/stories/BaseHook/index.tsx @@ -1,6 +1,7 @@ import { useState } from "react"; import { restrictToWindowEdges } from "@dnd-kit/modifiers"; import { css } from "@emotion/css"; +import { NodeProps } from "reactflow"; import { HvButton, HvGlobalActions, @@ -19,6 +20,7 @@ import { } from "@hitachivantara/uikit-react-icons"; import { HvFlow, + HvFlowBackground, HvFlowControls, HvFlowEmpty, HvFlowProps, @@ -26,7 +28,6 @@ import { } from "@hitachivantara/uikit-react-lab"; import { restrictToSample } from "../Base"; -import { LayoutsProvider } from "../Base/LayoutsContext"; // The code for these utils are available here: https://github.com/lumada-design/hv-uikit-react/tree/master/packages/lab/src/components/Flow/stories/BaseHook import { CustomEdge } from "./IconEdge"; import { Node } from "./Node"; @@ -54,40 +55,34 @@ const initialState = { edges: [ { source: "1caf2381eaf", - sourceHandle: "0", + sourceHandle: "leaves", target: "caf2381eaf3", - targetHandle: "0", + targetHandle: "teapot", id: "reactflow__edge-1caf2381eaf0-caf2381eaf30", type: "iconEdge", }, ], - viewport: { x: 150, y: 300, zoom: 0.6 }, + viewport: { x: 100, y: 500, zoom: 1 }, }; -const LeavesNode = (props) => ( +const LeavesNode = (props: NodeProps) => ( ); -const WaterNode = (props) => ( - +const WaterNode = (props: NodeProps) => ( + ); -const TeapotNode = (props) => ( +const TeapotNode = (props: NodeProps) => ( ); @@ -112,14 +107,14 @@ const nodeGroups = { }, } satisfies HvFlowProps["nodeGroups"]; -export type NodeGroup = keyof typeof nodeGroups; - -export const nodeTypes: HvFlowProps["nodeTypes"] = { +const nodeTypes = { leaves: LeavesNode, teapot: TeapotNode, water: WaterNode, } satisfies HvFlowProps["nodeTypes"]; +const edgeTypes = { iconEdge: CustomEdge } satisfies HvFlowProps["edgeTypes"]; + // Classes export const classes = { root: css({ height: "100vh" }), @@ -127,30 +122,14 @@ export const classes = { flow: css({ height: "calc(100% - 90px)", }), + customAction: css({ display: "flex", flexDirection: "row" }), }; -export const Flow = () => { +export const BaseHook = () => { const { rootId } = useTheme(); const [open, setOpen] = useState(false); - const CustomAction = ( -
- { - e.preventDefault(); - setOpen(true); - }} - > - Add nodes - -  to start building your flow. -
- ); - return (
{ { }} /> } - // Keeping track of flow updates - onFlowChange={(nds, eds) => - console.log("Flow updated: ", { nodes: nds, edges: eds }) - } > + + { + e.preventDefault(); + setOpen(true); + }} + > + Add nodes + +  to start building your flow. +
+ } icon={} /> @@ -210,9 +201,3 @@ export const Flow = () => {
); }; - -export const BaseHook = () => ( - - - -); diff --git a/packages/pentaho/src/Canvas/Canvas.mdx b/packages/pentaho/src/Canvas/Canvas.mdx new file mode 100644 index 0000000000..0d112f842d --- /dev/null +++ b/packages/pentaho/src/Canvas/Canvas.mdx @@ -0,0 +1,16 @@ +import { Meta } from "@storybook/addon-docs"; + + + +# Canvas + +This section showcases all the Pentaho+ canvas components. At the moment, the following components are available: + +- [Floating Panel](./?path=/docs/pentaho-canvas-floating-panel--docs) +- [Panel](./?path=/docs/pentaho-canvas-panel--docs) +- [Tabs and Tab](./?path=/docs/pentaho-canvas-tabs--docs) +- [Toolbar](./?path=/docs/pentaho-canvas-toolbar--docs) + +As a disclaimer, it's important to note that all canvas components are a work in progress and there might be breaking changes. + +A sample that exemplifies how the canvas components can be used is available [here](./?path=/docs/templates-canvas--docs). diff --git a/packages/pentaho/src/Canvas/stories/Canvas.stories.tsx b/packages/pentaho/src/Canvas/stories/Canvas.stories.tsx deleted file mode 100644 index 86ce95e9e7..0000000000 --- a/packages/pentaho/src/Canvas/stories/Canvas.stories.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Meta, StoryObj } from "@storybook/react"; -import { userEvent, within } from "@storybook/test"; - -import { MainStory } from "./Main"; -import MainRaw from "./Main?raw"; - -const meta: Meta = { - title: "Pentaho/Canvas", - parameters: { - // Enables Chromatic snapshot - chromatic: { disableSnapshot: false }, - }, -}; -export default meta; - -export const Main: StoryObj = { - decorators: [ - (Story) => ( -
- {Story()} -
- ), - ], - parameters: { - docs: { - description: { - story: `This sample exemplifies how the canvas components can be used.
- \nDISCLAIMER: Canvas components are a work in progress and there might be breaking changes.`, - }, - source: { - code: MainRaw, - }, - }, - }, - // For visual testing - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - const openBtn = canvas.getByRole("button", { - name: /Open/i, - }); - await userEvent.click(openBtn); - const toggleBtn = canvas.getByRole("button", { - name: /Toggle/i, - }); - await userEvent.click(toggleBtn); - }, - render: () => , -}; diff --git a/packages/pentaho/src/Canvas/stories/ListView/ListView.tsx b/packages/pentaho/src/Canvas/stories/ListView/ListView.tsx deleted file mode 100644 index 334d83c9e7..0000000000 --- a/packages/pentaho/src/Canvas/stories/ListView/ListView.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import { HTMLAttributes, useMemo, useState } from "react"; -import { - DndContext, - DragOverEvent, - DragOverlay, - DragStartEvent, - KeyboardSensor, - Modifier, - PointerSensor, - useSensor, - useSensors, -} from "@dnd-kit/core"; -import { restrictToWindowEdges } from "@dnd-kit/modifiers"; -import { - arrayMove, - SortableContext, - sortableKeyboardCoordinates, - useSortable, -} from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; -import { css } from "@emotion/css"; -import { - HvInput, - HvListContainer, - HvListItem, - theme, - useTheme, -} from "@hitachivantara/uikit-react-core"; -import { Drag, Search } from "@hitachivantara/uikit-react-icons"; - -import { sampleItems } from "./sampleData"; -import classes from "./styles"; - -export type Column = { - id: string; - title?: string; -}; - -export type Item = { - id: string; - columnId?: Column["id"]; - title?: string; - icon?: React.ReactNode; -}; - -// #region Fixes a problem we have while dragging items in storybook docs mode -type RestrictToSampleModifier = Modifier extends (...args: infer A) => infer R - ? (rootId: string, ...args: A) => R - : unknown; - -export const restrictToSample: RestrictToSampleModifier = ( - rootId, - { transform }, -) => { - const rect = document.getElementById(rootId)?.getBoundingClientRect(); - - const docsMode = window.location.search.includes("?viewMode=docs"); - - return { - ...transform, - x: docsMode && rect?.x ? -rect.x + transform.x : transform.x, - y: docsMode && rect?.y ? -rect.y + transform.y : transform.y, - }; -}; -// #endregion - -interface ItemProps extends HTMLAttributes { - item: Item; -} - -export const ItemCard = ({ item, style: overlayStyle }: ItemProps) => { - const { - setNodeRef, - attributes, - listeners, - transform, - transition, - isDragging, - } = useSortable({ id: item.id, data: { type: "Item", item } }); - - const style = { - transition, - transform: CSS.Transform.toString(transform), - }; - - return ( - -
-
{item.icon}
- {item.title} -
- -
- ); -}; - -export const ListView = () => { - const [items, setItems] = useState(sampleItems.filter((i) => i.id)); - const [activeItem, setActiveItem] = useState(null); - const { rootId } = useTheme(); - - const itemsIds = useMemo(() => items?.map((task) => task.id), [items]) || []; - - const sensors = useSensors( - useSensor(PointerSensor, { - activationConstraint: { - distance: 2, - }, - }), - useSensor(KeyboardSensor, { - // the `sortableKeyboardCoordinates` function moves the active draggable - // item to the closest sortable element in a given direction - coordinateGetter: sortableKeyboardCoordinates, - }), - ); - - const onDragStart = (event: DragStartEvent) => { - if (event.active.data.current?.type === "Item") { - setActiveItem(event.active.data.current.item); - } - }; - - const onDragEnd = () => { - setActiveItem(null); - }; - - const onDragOver = (event: DragOverEvent) => { - const { active, over } = event; - if (!over) return; - - const activeId = active.id; - const overId = over.id; - - if (activeId === overId) return; - - const isActiveAnItem = active.data.current?.type === "Item"; - const isOverAnItem = over.data.current?.type === "Item"; - - if (!isActiveAnItem) return; - - // Dropping an Item over another Item - if (isActiveAnItem && isOverAnItem) { - setItems((item) => { - const activeIndex = item.findIndex((t) => t.id === activeId); - const overIndex = item.findIndex((t) => t.id === overId); - return arrayMove(item, activeIndex, overIndex); - }); - } - }; - - return ( - -
-
- } - /> -
- - - {items && - items?.map((item) => )} - - -
- - restrictToSample(rootId || "", args), - ]} - > - {activeItem && ( - - )} - -
- ); -}; diff --git a/packages/pentaho/src/Canvas/stories/ListView/sampleData.tsx b/packages/pentaho/src/Canvas/stories/ListView/sampleData.tsx deleted file mode 100644 index db46e94eb9..0000000000 --- a/packages/pentaho/src/Canvas/stories/ListView/sampleData.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { - Battery, - Cloud, - Edit, - Favorite, - Fire, - Ghost, - Heart, - Palette, -} from "@hitachivantara/uikit-react-icons"; - -import { Item } from "./types"; - -export const sampleItems: Item[] = [ - { id: "item1", title: "Item 1", icon: }, - { id: "item2", title: "Item 2", icon: }, - { id: "item3", title: "Item 3", icon: }, - { id: "item4", title: "Item 4", icon: }, - { id: "item5", title: "Item 5", icon: }, - { id: "item6", title: "Item 6", icon: }, - { id: "item7", title: "Item 7", icon: }, - { id: "item8", title: "Item 8", icon: }, - { id: "item9", title: "Item 9", icon: }, -]; diff --git a/packages/pentaho/src/Canvas/stories/ListView/styles.tsx b/packages/pentaho/src/Canvas/stories/ListView/styles.tsx deleted file mode 100644 index c4988cb0dd..0000000000 --- a/packages/pentaho/src/Canvas/stories/ListView/styles.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { css } from "@emotion/css"; -import { outlineStyles, theme } from "@hitachivantara/uikit-react-core"; - -const styles = { - column: css({ - width: "100%", - borderRadius: theme.radii.round, - }), - columnHeader: css({ - marginBottom: theme.space.md, - }), - item: css({ - display: "flex", - justifyContent: "space-between", - alignItems: "center", - border: `1px solid ${theme.colors.atmo4}`, - borderRadius: theme.radii.round, - backgroundColor: theme.colors.atmo1, - margin: theme.spacing("xs", 0), - padding: 4, - height: "unset", - "&:focus-visible": { - ...outlineStyles, - }, - }), - itemTitle: css({ - display: "flex", - alignItems: "center", - gap: theme.space.xs, - }), - icon: css({ - borderRadius: theme.radii.base, - backgroundColor: theme.palette.green[100], - "& svg .color0": { - fill: theme.palette.green[700], - }, - }), -}; - -export default styles; diff --git a/packages/pentaho/src/Canvas/stories/ListView/types.ts b/packages/pentaho/src/Canvas/stories/ListView/types.ts deleted file mode 100644 index 990f650a4f..0000000000 --- a/packages/pentaho/src/Canvas/stories/ListView/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import React from "react"; - -export type Column = { - id: string; - title?: string; -}; - -export type Item = { - id: string; - columnId?: Column["id"]; - title?: string; - icon?: React.ReactNode; -}; diff --git a/packages/pentaho/src/Canvas/stories/Main.tsx b/packages/pentaho/src/Canvas/stories/Main.tsx deleted file mode 100644 index 9f8222e3c3..0000000000 --- a/packages/pentaho/src/Canvas/stories/Main.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import { useMemo, useState } from "react"; -import { cx } from "@emotion/css"; -import { BackgroundVariant } from "reactflow"; -import { - HvButton, - HvDialog, - HvDialogContent, - HvDialogTitle, - HvDropDownMenu, - HvIconButton, - HvInlineEditor, - theme, -} from "@hitachivantara/uikit-react-core"; -import { - Backwards, - Calendar, - Close, - DropUpXS, - Fullscreen, - Plane, - Redo, - Undo, - User, -} from "@hitachivantara/uikit-react-icons"; -import { - HvFlow, - HvFlowBackground, - HvFlowMinimap, -} from "@hitachivantara/uikit-react-lab"; -import { - HvCanvasFloatingPanel, - HvCanvasFloatingPanelProps, - HvCanvasPanel, - HvCanvasToolbar, -} from "@hitachivantara/uikit-react-pentaho"; - -// The code is available here: https://github.com/lumada-design/hv-uikit-react/tree/master/packages/lab/src/Canvas/stories -import { eds, nds, nodeTypes } from "./nodes"; -import { classes } from "./styles"; -import { - floatingPanelContent, - floatingPanelTabs, - panelContent, - panelTabs, - Separator, -} from "./utils"; - -export const MainStory = () => { - // --- State for Canvas Panel - const [panelTab, setPanelTab] = useState(panelTabs[0].id); - const [panelOpened, setPanelOpened] = useState(false); - - // --- State for Canvas Floating Panel - const [floatingPanelTab, setFloatingPanelTab] = useState( - floatingPanelTabs[0].id, - ); - const [floatingPanelOpened, setFloatingPanelOpened] = useState(false); - const [floatingTabs, setFloatingTabs] = - useState(floatingPanelTabs); - const [minimized, setMinimized] = useState(false); - const [fullscreen, setFullscreen] = useState(false); - - const handleCloseTab = (value: string | number) => { - const newFloatingTabs = floatingTabs.filter((tab) => tab.id !== value); - if (newFloatingTabs.length !== 0) { - setFloatingTabs(newFloatingTabs); - setFloatingPanelTab(newFloatingTabs[0].id as number); - } else { - setFloatingPanelOpened(false); - } - }; - - const handleChangeTab: HvCanvasFloatingPanelProps["onTabChange"] = ( - event, - value, - ) => { - setFloatingPanelTab(value as number); - }; - - const handleAction: HvCanvasFloatingPanelProps["onAction"] = ( - event, - action, - tabId, - ) => { - switch (action.id) { - case "close": - event.stopPropagation(); - handleCloseTab(tabId); - break; - case "toggle": - if (minimized && floatingPanelTab !== tabId) - handleChangeTab(null, tabId); - setMinimized((prev) => !prev); - break; - case "fullscreen": - if (minimized && floatingPanelTab !== tabId) - handleChangeTab(null, tabId); - setFullscreen((prev) => !prev); - break; - default: - break; - } - }; - - const dialogTitle = useMemo( - () => floatingPanelTabs.find((tab) => tab.id === floatingPanelTab)?.title, - [floatingPanelTab], - ); - - return ( - <> - - - - - - - - } - title={} - > - - - - - - - - Save - { - setFloatingPanelOpened(!floatingPanelOpened); - setFloatingTabs(floatingPanelTabs); - setFloatingPanelTab(floatingPanelTabs[0].id); - }} - > - Toggle - - - console.log(item.label)} - dataList={[ - { label: "Label 1", icon: }, - { label: "Label 2", icon: , disabled: true }, - { label: "Label 3", icon: }, - ]} - /> - - setPanelTab(value as number)} - onToggle={(event, value) => setPanelOpened(value)} - > - {panelContent[panelTab]} - - - ), - }, - ]} - rightActions={[ - { - id: "fullscreen", - label: "Fullscreen", - icon: , - }, - { - id: "close", - label: "Close", - icon: , - }, - ]} - onTabChange={handleChangeTab} - onAction={handleAction} - > - {floatingPanelContent[floatingPanelTab]} - - setFullscreen((prev) => !prev)} - > - - {dialogTitle} - - - {floatingPanelContent[floatingPanelTab]} - - - - ); -}; diff --git a/packages/pentaho/src/Canvas/stories/TreeView.tsx b/packages/pentaho/src/Canvas/stories/TreeView.tsx deleted file mode 100644 index 7d245ba322..0000000000 --- a/packages/pentaho/src/Canvas/stories/TreeView.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { HvTreeItem, HvTreeView } from "@hitachivantara/uikit-react-core"; - -export const TreeView = () => { - return ( - - - - - - - - - - - - - - ); -}; diff --git a/packages/pentaho/src/Canvas/stories/nodes.tsx b/packages/pentaho/src/Canvas/stories/nodes.tsx deleted file mode 100644 index df2b8f3571..0000000000 --- a/packages/pentaho/src/Canvas/stories/nodes.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Handle, Position } from "reactflow"; -import { theme } from "@hitachivantara/uikit-react-core"; -import { HvFlowProps } from "@hitachivantara/uikit-react-lab"; - -const Node = (props) => ( - <> -
- {props?.data.label} -
- - - -); - -export const nodeTypes = { - node: Node, -} satisfies HvFlowProps["nodeTypes"]; - -export const nds = [ - { - id: "1", - type: "node", - data: { label: "Node 1" }, - position: { x: 550, y: 250 }, - }, - { - id: "2", - type: "node", - data: { label: "Node 2" }, - position: { x: 400, y: 400 }, - }, - { - id: "3", - type: "node", - data: { label: "Node 3" }, - position: { x: 700, y: 400 }, - }, -]; - -export const eds = [ - { - id: "1-2", - source: "1", - sourceHandle: "0", - target: "2", - targetHandle: "0", - }, - { - id: "2-3", - source: "2", - sourceHandle: "0", - target: "3", - targetHandle: "0", - }, -]; diff --git a/packages/pentaho/src/Canvas/stories/styles.tsx b/packages/pentaho/src/Canvas/stories/styles.tsx deleted file mode 100644 index 9ff61cd375..0000000000 --- a/packages/pentaho/src/Canvas/stories/styles.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { css } from "@emotion/css"; -import { theme } from "@hitachivantara/uikit-react-core"; - -export const classes = { - separator: css({ - height: 30, - width: 1, - backgroundColor: theme.colors.atmo4, - margin: `0 ${theme.space.xs}`, - }), - flow: css({ width: "100%", height: "100%" }), - absoluteFull: css({ - left: 0, - right: 0, - marginLeft: "auto", - marginRight: "auto", - width: `calc(100% - 2 * ${theme.space.md})`, - }), - absoluteMin: css({ - right: theme.space.md, - width: `calc(100% - 320px - 2 * ${theme.space.md})`, - }), - toggleIcon: css({ transition: "rotate 0.2s ease" }), - titleContainer: css({ - display: "flex", - width: "100%", - }), - dialogTitle: css({ - ...theme.typography.label, - "& div > div": { margin: 0, padding: 0 }, - }), -}; diff --git a/packages/pentaho/src/Canvas/stories/utils.tsx b/packages/pentaho/src/Canvas/stories/utils.tsx deleted file mode 100644 index 6485f9a3fd..0000000000 --- a/packages/pentaho/src/Canvas/stories/utils.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { HvOverflowTooltip } from "@hitachivantara/uikit-react-core"; -import { DataSource, Schema } from "@hitachivantara/uikit-react-icons"; - -import { ListView } from "./ListView/ListView"; -import { classes } from "./styles"; -import { TreeView } from "./TreeView"; - -export const panelTabs = [ - { - id: 0, - content: ( - <> - - Add Data - - ), - }, - { - id: 2, - content: ( - <> - - Model Structure - - ), - }, -]; -export const panelContent = { - [panelTabs[0].id]: , - [panelTabs[1].id]: , -}; - -const TitleContainer = ({ children }: { children: React.ReactNode }) => ( -
- -
-); - -export const floatingPanelTabs = [ - { - id: 0, - title: This is an extremely long tab 1, - }, - { - id: 1, - title: Tab 2, - }, -]; -export const floatingPanelContent = { - [floatingPanelTabs[0].id]: ( -
- Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam quam - accusantium id architecto culpa libero, sit rem soluta autem veritatis - animi quaerat exercitationem reiciendis. Obcaecati ab quas nostrum sit - quisquam. -
- ), - [floatingPanelTabs[1].id]: ( -
- Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eveniet quasi - animi earum illo ullam quaerat assumenda cum. Eligendi laudantium illum, - earum corrupti porro incidunt illo ullam odit reiciendis id natus. -
- ), -}; - -export const Separator = () =>
; diff --git a/packages/styles/src/types.ts b/packages/styles/src/types.ts index fb55d123b1..cf7a6a4385 100644 --- a/packages/styles/src/types.ts +++ b/packages/styles/src/types.ts @@ -110,7 +110,7 @@ export type SpacingValue = number | HvThemeBreakpoint | (string & {}); export type HvThemeColors = typeof colors.common & typeof colors.light; // Base themes: DS3 and DS5 -export type HvBaseTheme = "ds3" | "ds5"; +export type HvBaseTheme = "ds3" | "ds5" | "pentahoPlus"; // Theme color modes export type HvThemeColorMode = "dawn" | "wicked";