Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Notebook Node actions #17520

Merged
merged 18 commits into from
Sep 27, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 47 additions & 17 deletions frontend/src/scenes/notebooks/Nodes/NodeWrapper.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,62 @@
--border-color: var(--border);

transform: translate3d(0, 0, 0);
border: 1px solid var(--border-color);
border-radius: var(--radius);
margin: 0px 0px 1rem 0px;
background-color: var(--bg-light);
transition: border 150ms linear;

.NotebookNode__meta {
.NotebookNode__box {
transform: translate3d(0, 0, 0);
border: 1px solid var(--border-color);
border-radius: var(--radius);
background-color: var(--bg-light);
transition: border 150ms linear, margin-bottom 150ms linear;

.NotebookNode__meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.75rem;
font-weight: 700;
color: var(--muted-alt-3000);
padding: var(--notebook-node-meta-padding);
border-bottom: 1px solid var(--border);
height: var(--notebook-node-meta-height);
transition: height 150ms ease-out;
overflow: hidden;
}

.NotebookNode__content {
transition: box-shadow 150ms ease-out;
z-index: 1;
}
}

.NotebookNode__actions {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.75rem;
font-weight: 700;
color: var(--muted-alt-3000);
padding: var(--notebook-node-meta-padding);
border-bottom: 1px solid var(--border);
height: var(--notebook-node-meta-height);
transition: height 150ms ease-out;
gap: 0.25rem;
overflow: hidden;

transition: all 150ms linear 1000ms;
opacity: 0;
height: 0;
margin-top: 0;
}

.NotebookNode__content {
transition: box-shadow 150ms ease-out;
&:hover,
&--selected {
.NotebookNode__actions {
opacity: 1;
height: 2rem;
margin-top: 0.5rem;
transition: all 150ms linear;
}
}

&--selected {
--border-color: var(--primary-3000);
}

&--auto-hide-metadata {
border-color: transparent;
--border-color: transparent;

.NotebookNode__meta {
height: 0;
Expand All @@ -55,6 +81,10 @@
}
}
}

&:hover {
--border-color: var(--border);
}
}
}

Expand Down
198 changes: 116 additions & 82 deletions frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@ import {
} from '@tiptap/react'
import { ReactNode, useCallback, useRef } from 'react'
import clsx from 'clsx'
import { IconClose, IconDragHandle, IconFilter, IconLink, IconUnfoldLess, IconUnfoldMore } from 'lib/lemon-ui/icons'
import {
IconClose,
IconDragHandle,
IconFilter,
IconLink,
IconPlusMini,
IconUnfoldLess,
IconUnfoldMore,
} from 'lib/lemon-ui/icons'
import { LemonButton } from '@posthog/lemon-ui'
import './NodeWrapper.scss'
import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton'
Expand Down Expand Up @@ -49,7 +57,7 @@ export interface NodeWrapperProps<T extends CustomNotebookNodeAttributes> {
widgets?: NotebookNodeWidget[]
}

export function NodeWrapper<T extends CustomNotebookNodeAttributes>({
function NodeWrapper<T extends CustomNotebookNodeAttributes>({
defaultTitle,
nodeType,
children,
Expand All @@ -69,8 +77,8 @@ export function NodeWrapper<T extends CustomNotebookNodeAttributes>({
widgets = [],
}: NodeWrapperProps<T> & NotebookNodeViewProps<T>): JSX.Element {
const mountedNotebookLogic = useMountedLogic(notebookLogic)
const { isEditable, editingNodeId } = useValues(mountedNotebookLogic)
const { setEditingNodeId } = useActions(mountedNotebookLogic)
const { isEditable, editingNodeId } = useValues(notebookLogic)
const { setEditingNodeId } = useActions(notebookLogic)

// nodeId can start null, but should then immediately be generated
const nodeId = attributes.nodeId
Expand All @@ -87,7 +95,7 @@ export function NodeWrapper<T extends CustomNotebookNodeAttributes>({
startExpanded,
}
const nodeLogic = useMountedLogic(notebookNodeLogic(nodeLogicProps))
const { resizeable, expanded } = useValues(nodeLogic)
const { resizeable, expanded, actions } = useValues(nodeLogic)
const { setExpanded, deleteNode } = useActions(nodeLogic)

const [ref, inView] = useInView({ triggerOnce: true })
Expand Down Expand Up @@ -125,94 +133,120 @@ export function NodeWrapper<T extends CustomNotebookNodeAttributes>({
return (
<NotebookNodeContext.Provider value={nodeLogic}>
<BindLogic logic={notebookNodeLogic} props={nodeLogicProps}>
<NodeViewWrapper
ref={ref}
as="div"
className={clsx(nodeType, 'NotebookNode', {
'NotebookNode--selected': isEditable && selected,
'NotebookNode--auto-hide-metadata': autoHideMetadata,
})}
>
<ErrorBoundary>
{!inView ? (
<>
<div className="h-4" /> {/* Placeholder for the drag handle */}
{/* eslint-disable-next-line react/forbid-dom-props */}
<div style={{ height: heightEstimate }}>
<LemonSkeleton className="h-full" />
</div>
</>
) : (
<>
<div className="NotebookNode__meta" data-drag-handle>
<LemonButton
onClick={() => setExpanded(!expanded)}
size="small"
status="primary-alt"
className="flex-1"
icon={
isEditable ? (
<IconDragHandle className="cursor-move text-base shrink-0" />
) : undefined
}
>
<span className="flex-1 cursor-pointer">{title}</span>
</LemonButton>

<div className="flex space-x-1">
{parsedHref && <LemonButton size="small" icon={<IconLink />} to={parsedHref} />}

{expandable && (
<NodeViewWrapper as="div">
<div
ref={ref}
className={clsx(nodeType, 'NotebookNode', {
'NotebookNode--selected': isEditable && selected,
'NotebookNode--auto-hide-metadata': autoHideMetadata,
})}
>
<div className="NotebookNode__box">
<ErrorBoundary>
{!inView ? (
<>
<div className="h-4" /> {/* Placeholder for the drag handle */}
{/* eslint-disable-next-line react/forbid-dom-props */}
<div style={{ height: heightEstimate }}>
<LemonSkeleton className="h-full" />
</div>
</>
) : (
<>
<div className="NotebookNode__meta" data-drag-handle>
<LemonButton
onClick={() => setExpanded(!expanded)}
size="small"
icon={expanded ? <IconUnfoldLess /> : <IconUnfoldMore />}
/>
)}

{widgets.length > 0 ? (
<LemonButton
onClick={() =>
setEditingNodeId(editingNodeId === nodeId ? null : nodeId)
status="primary-alt"
className="flex-1"
icon={
isEditable ? (
<IconDragHandle className="cursor-move text-base shrink-0" />
) : undefined
}
size="small"
icon={<IconFilter />}
active={editingNodeId === nodeId}
/>
) : null}
>
<span className="flex-1 cursor-pointer">{title}</span>
</LemonButton>

{isEditable && (
<LemonButton
onClick={() => deleteNode()}
size="small"
status="danger"
icon={<IconClose />}
/>
)}
</div>
</div>
<div
ref={contentRef}
className={clsx(
'NotebookNode__content flex flex-col relative z-0 overflow-hidden',
isEditable && isResizeable && 'resize-y'
)}
// eslint-disable-next-line react/forbid-dom-props
style={isResizeable ? { height, minHeight } : {}}
onClick={!expanded && expandOnClick ? () => setExpanded(true) : undefined}
onMouseDown={onResizeStart}
>
{children}
</div>
</>
)}
</ErrorBoundary>
<div className="flex space-x-1">
{parsedHref && (
<LemonButton size="small" icon={<IconLink />} to={parsedHref} />
)}

{expandable && (
<LemonButton
onClick={() => setExpanded(!expanded)}
size="small"
icon={expanded ? <IconUnfoldLess /> : <IconUnfoldMore />}
/>
)}

{widgets.length > 0 ? (
<LemonButton
onClick={() =>
setEditingNodeId(editingNodeId === nodeId ? null : nodeId)
}
size="small"
icon={<IconFilter />}
active={editingNodeId === nodeId}
/>
) : null}

{isEditable && (
<LemonButton
onClick={() => deleteNode()}
size="small"
status="danger"
icon={<IconClose />}
/>
)}
</div>
</div>
<div
ref={contentRef}
className={clsx(
'NotebookNode__content flex flex-col relative z-0 overflow-hidden',
isEditable && isResizeable && 'resize-y'
)}
// eslint-disable-next-line react/forbid-dom-props
style={isResizeable ? { height, minHeight } : {}}
onClick={!expanded && expandOnClick ? () => setExpanded(true) : undefined}
onMouseDown={onResizeStart}
>
{children}
</div>
</>
)}
</ErrorBoundary>
</div>
{actions.length ? (
<div className="NotebookNode__actions">
{actions.map((x, i) => (
<LemonButton
key={i}
type="secondary"
status="primary"
size="small"
icon={x.icon ?? <IconPlusMini />}
onClick={(e) => {
e.stopPropagation()
x.onClick()
}}
>
{x.text}
</LemonButton>
))}
</div>
) : null}
</div>
</NodeViewWrapper>
</BindLogic>
</NotebookNodeContext.Provider>
)
}

// const MemoizedNodeWrapper = memo(NodeWrapper) as typeof NodeWrapper

export type CreatePostHogWidgetNodeOptions<T extends CustomNotebookNodeAttributes> = NodeWrapperProps<T> & {
nodeType: NotebookNodeType
Component: (props: NotebookNodeViewProps<T>) => JSX.Element | null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createPostHogWidgetNode } from 'scenes/notebooks/Nodes/NodeWrapper'
import { EarlyAccessFeatureStage, EarlyAccessFeatureType, NotebookNodeType } from '~/types'
import { BindLogic, useActions, useValues } from 'kea'
import { IconFlag, IconRocketLaunch } from 'lib/lemon-ui/icons'
import { LemonButton, LemonDivider, LemonTag } from '@posthog/lemon-ui'
import { LemonDivider, LemonTag } from '@posthog/lemon-ui'
import { urls } from 'scenes/urls'
import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton'
import { notebookNodeLogic } from './notebookNodeLogic'
Expand All @@ -19,7 +19,23 @@ const Component = (props: NotebookNodeViewProps<NotebookNodeEarlyAccessAttribute
const { id } = props.attributes
const { earlyAccessFeature, earlyAccessFeatureLoading } = useValues(earlyAccessFeatureLogic({ id }))
const { expanded } = useValues(notebookNodeLogic)
const { insertAfter } = useActions(notebookNodeLogic)
const { insertAfter, setActions } = useActions(notebookNodeLogic)

useEffect(() => {
const flagId = (earlyAccessFeature as EarlyAccessFeatureType).feature_flag?.id

setActions(
flagId
? [
{
text: 'View feature flag',
icon: <IconFlag />,
onClick: () => insertAfter(buildFlagContent(flagId)),
},
]
: []
)
}, [earlyAccessFeature])

useEffect(() => {
props.updateAttributes({
Expand Down Expand Up @@ -88,24 +104,6 @@ const Component = (props: NotebookNodeViewProps<NotebookNodeEarlyAccessAttribute
)}
</>
) : null}

<LemonDivider className="my-0" />
<div className="p-2 mr-1 flex justify-end gap-2">
<LemonButton
type="secondary"
size="small"
icon={<IconFlag />}
onClick={() => {
insertAfter(
buildFlagContent(
(earlyAccessFeature as EarlyAccessFeatureType).feature_flag?.id || 'new'
)
)
}}
>
View Feature Flag
</LemonButton>
</div>
</BindLogic>
</div>
)
Expand Down
Loading