Skip to content

Commit

Permalink
Merge pull request #395 from nodepen/error-messages
Browse files Browse the repository at this point in the history
Runtime warning/error messages
  • Loading branch information
cdriesler authored Aug 21, 2023
2 parents 5798471 + cba1080 commit c87e0db
Show file tree
Hide file tree
Showing 24 changed files with 311 additions and 31 deletions.
17 changes: 4 additions & 13 deletions apps/rhino-compute-server/Endpoints/GrasshopperEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,26 +77,17 @@ public Response SolveGrasshopperDocument(NancyContext context)

nodeSolutionData.NodeRuntimeData.DurationMs = componentInstance.ProcessorTime.TotalMilliseconds;

foreach (string runtimeMessage in componentInstance.RuntimeMessages(GH_RuntimeMessageLevel.Error))
if (componentInstance.RuntimeMessageLevel != GH_RuntimeMessageLevel.Blank)
{
NodePenNodeRuntimeDataMessage nodeRuntimeMessage = new NodePenNodeRuntimeDataMessage()
{
Level = "error",
Message = runtimeMessage
Level = componentInstance.RuntimeMessageLevel.ToString().ToLower(),
Message = componentInstance.RuntimeMessages(componentInstance.RuntimeMessageLevel)[0]
};

nodeSolutionData.NodeRuntimeData.Messages.Add(nodeRuntimeMessage);
}

foreach (string runtimeMessage in componentInstance.RuntimeMessages(GH_RuntimeMessageLevel.Warning))
{
NodePenNodeRuntimeDataMessage nodeRuntimeMessage = new NodePenNodeRuntimeDataMessage()
{
Level = "warn",
Message = runtimeMessage
};

nodeSolutionData.NodeRuntimeData.Messages.Add(nodeRuntimeMessage);
Console.WriteLine(nodeSolutionData.NodeRuntimeData.Messages.FirstOrDefault().Message);
}

// Collect child param solution data
Expand Down
2 changes: 1 addition & 1 deletion packages/converters/models/NodePenNodeSolutionData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public class NodePenNodeRuntimeDataMessage : Base
/// The severity of the message raised.
/// </summary>
/// <remarks>
/// Valid values: "error" | "warn" | "info"
/// Valid values: "error" | "warning" | "info"
/// </remarks>
public string Level { get; set; }

Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/types/DocumentNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export type DocumentNode = {
dx: number
dy: number
}
/** The horizontal center of where the node label is drawn. */
labelDeltaX: {
dx: number
dy: 0
}
}
/** Data providers for inputs on this node. */
sources: {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/types/solution/NodeSolutionData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ type NodeRuntimeData = {
}

type NodeRuntimeDataMessage = {
level: 'error' | 'war' | 'info'
level: 'error' | 'warning' | 'info'
message: string
}
2 changes: 2 additions & 0 deletions packages/nodes/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useDispatch, useStore } from '$'
import type { NodesAppCallbacks } from '$'
import { ControlsContainer } from '@/components'
import { FileUploadOverlayContainer, PseudoShadowsContainer } from './views/common'
import { StaticDialogLayer } from './views/static/dialog-layer'

type NodesAppProps = {
document: NodePen.Document
Expand Down Expand Up @@ -89,6 +90,7 @@ const NodesAppInternal = React.memo(({ children }: NodesAppInternalProps) => {
<FileUploadOverlayContainer />
<ControlsContainer />
<PseudoShadowsContainer />
<StaticDialogLayer />
{children}
</div>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useDebugRender, useDraggableNode, useSelectableNode } from '../hooks'
import {
GenericNodeBody,
GenericNodePorts,
GenericNodeRuntimeMessage,
GenericNodeShadow,
GenericNodeSkeleton,
GenericNodeWires,
Expand Down Expand Up @@ -39,6 +40,7 @@ const GenericNode = ({ id, template }: GenericNodeProps): React.ReactElement =>
</>
) : (
<>
<GenericNodeRuntimeMessage node={node} />
<GenericNodeShadow node={node} template={template} />
<GenericNodeBody node={node} template={template} />
<GenericNodePorts node={node} template={template} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'
import type { DocumentNode } from '@nodepen/core'
import { useRuntimeMessages } from '../../hooks'
import { COLORS, DIMENSIONS } from '@/constants'
import { useImperativeEvent } from '@/hooks'
import { getNodeRuntimeMessageBubble } from '@/utils/node-geometry'
import { WiresMaskPortal } from '@/components/annotations/wire/components'
import { Dialog } from '@/views/components'

type GenericNodeRuntimeMessageProps = {
node: DocumentNode
}

export const GenericNodeRuntimeMessage = ({ node }: GenericNodeRuntimeMessageProps) => {
const { instanceId, anchors, position } = node
const { x, y } = position

const [showDialog, setShowDialog] = useState(false)

const messageContainerRef = useRef<SVGGElement>(null)

const messages = useRuntimeMessages(instanceId)

const currentMessage = messages[0]
const currentMessageLevel = currentMessage?.level

// Position of bottom-middle arrow "point"
const dx = anchors['labelDeltaX'].dx
const dy = -9

const s = DIMENSIONS.NODE_RUNTIME_MESSAGE_BUBBLE_SIZE

const handlePointerDown = useCallback(
(e: PointerEvent) => {
e.stopPropagation()
console.log(messages[0]?.message)

// TODO: Show message somehow
setShowDialog(true)
},
[messages]
)

useImperativeEvent(messageContainerRef, 'pointerdown', handlePointerDown)

const [isVisible, setIsVisible] = useState(false)
const [visibleMessageLevel, setVisibleMessageLevel] = useState<typeof currentMessageLevel>('info')

useEffect(() => {
if (!currentMessage) {
setIsVisible(false)
return
}

setVisibleMessageLevel(currentMessage.level)
setIsVisible(true)
}, [currentMessage])

const messageColors: Record<typeof currentMessageLevel, string> = {
error: COLORS.ERROR,
warning: COLORS.WARN,
info: COLORS.GREEN,
}

const tx = isVisible ? 0 : 80

return (
<>
<g
ref={messageContainerRef}
className="np-transition-transform np-duration-300 np-ease-out"
style={{ transform: `translateY(${tx}px)` }}
>
{getNodeRuntimeMessageBubble(node, messageColors[visibleMessageLevel])}
<rect
className={`${
visibleMessageLevel === 'error'
? 'np-fill-error hover:np-fill-error-2'
: 'np-fill-warn hover:np-fill-warn-2'
} hover:np-cursor-pointer np-pointer-events-auto`}
x={x + dx + 3 - s / 2}
y={y + dy - 3 - s}
width={s - 6}
height={s - 6}
rx={3}
ry={3}
/>
<svg
width={24}
height={24}
style={{
transform: `translate(${node.position.x + node.anchors['labelDeltaX'].dx - 12}px, ${
node.position.y - 44
}px)`,
}}
aria-hidden="true"
fill="none"
stroke={COLORS.DARK}
strokeWidth="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className="np-pointer-events-none"
>
<path
d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
strokeLinecap="round"
strokeLinejoin="round"
vectorEffect="non-scaling-stroke"
></path>
</svg>
</g>
<WiresMaskPortal>
<g className="np-transition-transform np-duration-300 np-ease-out" style={{ transform: `translateY(${tx}px)` }}>
{getNodeRuntimeMessageBubble(node, '#FFFFFF')}
</g>
</WiresMaskPortal>
{showDialog ? (
<Dialog onClose={() => setShowDialog(false)}>
<p className="np-font-sans np-font-medium np-text-dark np-text-md">{messages[0].message}</p>
</Dialog>
) : null}
</>
)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { GenericNodeBody } from './GenericNodeBody'
export { GenericNodeLabel } from './GenericNodeLabel'
export { GenericNodePorts } from './GenericNodePorts'
export { GenericNodeRuntimeMessage } from './GenericNodeRuntimeMessage'
export { GenericNodeShadow } from './GenericNodeShadow'
export { GenericNodeSkeleton } from './GenericNodeSkeleton'
export { GenericNodeWires } from './GenericNodeWires'
1 change: 1 addition & 0 deletions packages/nodes/src/components/nodes/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { useDebugRender } from './useDebugRender'
export { useDraggableNode } from './useDraggableNode'
export { usePort } from './usePort'
export { useRuntimeMessages } from './useRuntimeMessages'
export { useSelectableNode } from './useSelectableNode'
31 changes: 31 additions & 0 deletions packages/nodes/src/components/nodes/hooks/useRuntimeMessages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useRef } from 'react'
import type { NodeSolutionData } from '@nodepen/core'
import { useStore } from '$'

/**
* Returns the latest runtime messages for the given document node.
* @remarks While solution lifecycle status is `expired`, returns a cached version of the previous solution's messages.
*/
export const useRuntimeMessages = (nodeInstanceId: string): NodeSolutionData['nodeRuntimeData']['messages'] => {
const previousMessages = useRef<NodeSolutionData['nodeRuntimeData']['messages']>([])

return useStore((state) => {
if (state.lifecycle.solution === 'expired') {
return previousMessages.current
}

const nodeSolutionData = state.solution.nodeSolutionData.find(
(nodeSolution) => nodeSolution.nodeInstanceId === nodeInstanceId
)

if (!nodeSolutionData) {
return []
}

const nodeRuntimeMessages = nodeSolutionData.nodeRuntimeData.messages

previousMessages.current = nodeRuntimeMessages

return nodeRuntimeMessages
})
}
4 changes: 4 additions & 0 deletions packages/nodes/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ export const COLORS = {
DARKGREEN: '#093824',
/* Special */
ERROR: '#FF7171',
ERRORDARK: '#DD6363',
WARN: '#FFBE71',
WARNDARK: '#E3AF71',
} as const

export const DIMENSIONS = {
Expand All @@ -31,6 +33,8 @@ export const DIMENSIONS = {
NODE_PORT_HEIGHT: 40,
NODE_PORT_MINIMUM_WIDTH: 50,
NODE_PORT_RADIUS: 5,
/** NODE_LABEL_WIDTH + 2 */
NODE_RUNTIME_MESSAGE_BUBBLE_SIZE: 34,
} as const

export const STYLES = {
Expand Down
2 changes: 2 additions & 0 deletions packages/nodes/src/store/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export type NodesAppState = {
contextMenus: {
[menuKey: string]: ContextMenu
}
dialogRoot: React.RefObject<HTMLDivElement>
shadows: {
containerRef: React.RefObject<HTMLDivElement> | null
proxyRefs: {
Expand Down Expand Up @@ -180,6 +181,7 @@ export const initialState: NodesAppState = {
registry: {
canvasRoot: React.createRef<HTMLDivElement>(),
contextMenus: {},
dialogRoot: React.createRef<HTMLDivElement>(),
shadows: {
containerRef: null,
proxyRefs: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ export const getNodeDimensions = (
nodeTemplate: NodePen.NodeTemplate
): Pick<NodePen.DocumentNode, 'anchors' | 'dimensions'> => {
const nodeDimensions: Pick<NodePen.DocumentNode, 'anchors' | 'dimensions'> = {
anchors: {},
anchors: {
labelDeltaX: {
dx: 0,
dy: 0,
},
},
dimensions: {
width: 0,
height: 0,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react'
import type { DocumentNode } from '@nodepen/core'
import { DIMENSIONS } from '@/constants'

export const getNodeRuntimeMessageBubble = (node: DocumentNode, fillColor: string) => {
const { anchors, position } = node
const { x, y } = position

// Position of bottom-middle arrow "point"
const dx = anchors['labelDeltaX'].dx
const dy = -9

const s = DIMENSIONS.NODE_RUNTIME_MESSAGE_BUBBLE_SIZE

return (
<>
<rect
style={{
transform: 'rotate(45deg)',
transformOrigin: `${x + dx}px ${y + dy - s / 4}px`,
}}
x={x + dx - s / 4}
y={y + dy - s / 2}
width={s / 2}
height={s / 2}
fill={fillColor}
rx={2}
ry={2}
/>
<rect x={x + dx - s / 2} y={y + dy - 6 - s} width={s} height={s} fill={fillColor} rx={6} ry={6} />
</>
)
}
1 change: 1 addition & 0 deletions packages/nodes/src/utils/node-geometry/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { getNodeRuntimeMessageBubble } from './getNodeRuntimeMessageBubble'
7 changes: 6 additions & 1 deletion packages/nodes/src/utils/templates/createInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ export const createInstance = (template: NodePen.NodeTemplate): NodePen.Document
isEnabled: true,
isProvisional: false,
},
anchors: {},
anchors: {
labelDeltaX: {
dx: 0,
dy: 0,
},
},
values: {},
sources: {},
inputs: {},
Expand Down
35 changes: 35 additions & 0 deletions packages/nodes/src/views/components/dialog/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react'
import { DialogPortal } from './DialogPortal'

type DialogProps = {
title?: string
children: React.ReactNode
onClose: () => void
}

export const Dialog = ({ title, onClose, children }: DialogProps) => {
return (
<DialogPortal>
<div className="np-relative np-w-full np-h-full np-pointer-events-auto">
<div
className="np-absolute np-left-0 np-top-0 np-w-full np-h-full np-z-0 np-bg-dark np-bg-opacity-30 hover:np-bg-opacity-40 np-transition-all np-duration-300 np-ease-out"
onClick={() => {
onClose()
}}
role="presentation"
></div>
<div className="np-absolute np-left-0 np-top-0 np-w-full np-h-full np-z-10 np-pointer-events-none">
<div className="np-w-full np-h-full np-flex np-justify-center np-items-center">
<div
className="np-w-96 np-p-4 np-bg-light np-rounded-md np-shadow-modal np-pointer-events-auto"
onPointerDown={(e) => e.stopPropagation()}
onDoubleClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>
</div>
</div>
</DialogPortal>
)
}
Loading

0 comments on commit c87e0db

Please sign in to comment.