[go: nahoru, domu]

Skip to content

Commit

Permalink
feat: Add the ability to ignore nodes from visualization (#108)
Browse files Browse the repository at this point in the history
* feat: add the ability to ignore nodes from visualization

* Comment data line

* Commit generated folder
  • Loading branch information
gabotechs committed Jun 30, 2024
1 parent ca29421 commit 3ff0045
Show file tree
Hide file tree
Showing 7 changed files with 403 additions and 239 deletions.
450 changes: 225 additions & 225 deletions internal/entropy/generated/index.html

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions web/src/@utils/useForceUpdate.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";

export function useForceUpdate() {
const [, updateState] = React.useState({});
return React.useCallback(() => updateState({}), []);
const [updateForced, updateState] = React.useState({});
return [updateForced as never, React.useCallback(() => updateState({}), [])]
}
15 changes: 12 additions & 3 deletions web/src/App.tsx

Large diffs are not rendered by default.

116 changes: 109 additions & 7 deletions web/src/Explorer/Explorer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CSSProperties, HTMLProps, ReactNode, useEffect, useMemo } from "react";
import React, { CSSProperties, HTMLProps, ReactNode, useEffect, useMemo, useRef, useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faFile, faFolder } from "@fortawesome/free-solid-svg-icons";
import { faGolang, faJs, faPython, faRust, IconDefinition } from "@fortawesome/free-brands-svg-icons";
Expand All @@ -14,7 +14,8 @@ const ID_PREFIX = '__explorer_'
enum TAGS {
SELECTED = 's',
HIGHLIGHTED = 'h',
EXPANDED = 'x'
EXPANDED = 'x',
IGNORE = 'i'
}

enum VALUES {
Expand Down Expand Up @@ -57,6 +58,7 @@ export interface ExplorerProps {
selected?: XNode
highlighted?: Set<XNode>
onSelectNode?: (x: XNode) => void
onNodesMutated?: () => void
}

export function Explorer (
Expand All @@ -65,7 +67,8 @@ export function Explorer (
fileTree,
onSelectNode,
highlighted,
selected
selected,
onNodesMutated
}: ExplorerProps
) {
const folderState = useMemo(() => {
Expand Down Expand Up @@ -111,39 +114,130 @@ export function Explorer (
}
}, [folderState, highlighted, selected])

const [contextMenuProps, setContextMenuProps] = useState<ContextMenuProps>()

return (
<div
className={`${className} flex flex-col overflow-y-scroll pb-8 pt-1 scrollbar-thin scrollbar-transparent`}
dir={'rtl'}
>
<ExplorerFolder folderState={folderState} onSelectNode={onSelectNode} dir={'ltr'}/>
{contextMenuProps && <ContextMenu
{...contextMenuProps}
onClose={() => setContextMenuProps(undefined)}
onNodesMutated={onNodesMutated}
/>}
<ExplorerFolder
folderState={folderState}
onSelectNode={onSelectNode}
onRightClick={setContextMenuProps}
dir={'ltr'}
/>
</div>
)
}

export interface ContextMenuProps {
x: number
y: number
folderState: FolderState<XNode>
onClose?: () => void
onNodesMutated?: () => void
}

function ContextMenu (
{
onClose,
x,
y,
folderState,
onNodesMutated
}: ContextMenuProps) {
const menuRef = useRef<HTMLDivElement>(null);

useEffect(() => {
function handleOutsideClick (event: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
onClose?.();
}
}

document.addEventListener('mousedown', handleOutsideClick);
return () => document.removeEventListener('mousedown', handleOutsideClick)
}, [onClose]);

function ignore () {
for (const node of folderState.allFiles()) {
node.ignore = true
node.links?.forEach(link => (link.ignore = true))
}
folderState.tagAll(TAGS.IGNORE, VALUES.YES)
onClose?.()
onNodesMutated?.()
}

function unIgnore () {
for (const node of folderState.allFiles()) {
node.ignore = false
node.links?.forEach(link => (link.ignore = false))
}
folderState.untagAll(TAGS.IGNORE)
onClose?.()
onNodesMutated?.()
}

return (
<div
ref={menuRef}
className="absolute bg-white border border-gray-300 z-50 rounded shadow-md w-[100px]"
style={{ top: y, left: x }}
>
<ul className="space-y-2">
<li
className="cursor-pointer px-2 py-1 hover:bg-gray-200 text-center"
onClick={folderState.tags[TAGS.IGNORE] ? unIgnore : ignore}
>
{folderState.tags[TAGS.IGNORE] ? 'un-ignore' : 'ignore'}
</li>
</ul>
</div>
);
}

interface ExplorerFolderProps {
onSelectNode?: (x: XNode) => void
onRightClick?: (props: ContextMenuProps) => void
folderState: FolderState<XNode>
}

function ExplorerFolder (
{
folderState,
onSelectNode,
onRightClick,
style,
...props
}: ExplorerFolderProps & HTMLProps<HTMLDivElement>) {
const forceUpdate = useForceUpdate()
const [, forceUpdate] = useForceUpdate()

useEffect(() => folderState.registerListener('update', forceUpdate), [folderState, forceUpdate]);

function onContextMenu (e: React.MouseEvent<HTMLDivElement>) {
e.preventDefault()
onRightClick?.({
folderState,
x: e.clientX,
y: e.clientY,
})
}

if (!folderState.tags[TAGS.EXPANDED]) {
return <Folder
name={folderState.name}
tags={folderState.tags}
logoColor={folderState.color}
style={style}
onClick={() => folderState.tag(TAGS.EXPANDED, VALUES.YES)} dir={props.dir}
onContextMenu={onContextMenu}
{...props}
/>
}
Expand All @@ -154,13 +248,15 @@ function ExplorerFolder (
logoColor={folderState.color}
tags={folderState.tags}
onClick={() => folderState.untagAllFolders(TAGS.EXPANDED)}
onContextMenu={onContextMenu}
/>
{[...folderState.folders.values()].map(folder =>
<ExplorerFolder
style={{ marginLeft: 16 }}
key={folder.name}
folderState={folder}
onSelectNode={onSelectNode}
onRightClick={onRightClick}
/>
)}
{[...folderState.files.values()].map(file =>
Expand Down Expand Up @@ -191,10 +287,12 @@ function Folder (
tags: Record<string, string>
} & HTMLProps<HTMLDivElement>) {
let backgroundColor: CSSProperties['color'] = undefined
let opacity = 1
if (tags[TAGS.SELECTED] === VALUES.YES) backgroundColor = COLORS.DIR_SELECTED
if (tags[TAGS.IGNORE] === VALUES.YES) opacity = 0.2
return <div
className={'flex flex-row items-center cursor-pointer'}
style={{ backgroundColor, ...style }}
style={{ backgroundColor, opacity, ...style }}
{...props}
>
<FontAwesomeIcon icon={faFolder} color={logoColor}/>
Expand All @@ -217,6 +315,7 @@ function File (
const ext = useMemo(() => name.split('.').slice(-1)[0], [name])
let backgroundColor: CSSProperties['color'] = undefined
let animation: ReactNode = null
let opacity = 1
if (tags?.[TAGS.HIGHLIGHTED] === VALUES.OUT) {
backgroundColor = COLORS.FILE_OUT_HIGHLIGHTED
animation = <AnimatedDot dir={'ltr'}/>
Expand All @@ -233,9 +332,12 @@ function File (
backgroundColor = COLORS.FILE_SELECTED
animation = null
}
if (tags?.[TAGS.IGNORE] === VALUES.YES) {
opacity = 0.2
}
return <div
className='flex flex-row items-center cursor-pointer'
style={{ backgroundColor, ...style }}
style={{ backgroundColor, opacity, ...style }}
{...props}
>
<FontAwesomeIcon icon={FA_MAP[ext] ?? faFile} color={logoColor}/>
Expand Down
30 changes: 30 additions & 0 deletions web/src/Explorer/FolderState.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,36 @@ d.ts undefined`,
}
)

it(
'Untag does not remove all parent tag folders',
{
nodes: [
['foo', 'bar', 'a.ts'],
['foo', 'baz', 'b.ts'],
['_']
],
squash: true,
modify: folderState => {
folderState.tagAllFolders('expanded', 'true')
folderState.folders.get('foo')!.folders.get('bar')!.untag('expanded')
folderState.untagAllFolders('expanded')
}
},
{
render: `\
> foo {}
> bar {}
a.ts undefined
> baz {}
b.ts undefined
_ undefined`,
events: [
['tagged', 'expanded', 'true'],
['untagged', 'expanded']
]
}
)

it(
'Multiple operations',
{
Expand Down
25 changes: 23 additions & 2 deletions web/src/Explorer/FolderState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ export class FolderState<F> {
return this._fileTags;
}

/** record for accessing nested tags in O(1) time */
/**
* record for accessing nested tags in O(1) time.
*
* This is a record from tag -> folder set
*/
private taggedFolders: Record<string, Set<string>> = {}
private parent?: FolderState<F>

Expand All @@ -51,6 +55,14 @@ export class FolderState<F> {
this.tag(tag, value)
}

tagAll (tag: string, value: string) {
for (const folder of this.folders.values()) folder.tagAll(tag, value)
this.tag(tag, value)
for (const file of this.files.keys()) {
this.tagFile(file, tag, value)
}
}

untagAllFolders (tag: string) {
for (const folder of this.taggedFolders[tag]?.values() ?? []) {
this.folders.get(folder)?.untagAllFolders(tag)
Expand All @@ -71,7 +83,7 @@ export class FolderState<F> {
untag (tag: string) {
delete this._tags[tag]
if (this.parent !== undefined) {
delete this.parent.taggedFolders[tag]
this.parent.taggedFolders[tag]?.delete(this.name)
}
this.notifyListeners(['untagged', tag])
}
Expand Down Expand Up @@ -122,6 +134,15 @@ export class FolderState<F> {
}
}

* allFiles (): Generator<F> {
for (const folder of this.folders.values()) {
yield * folder.allFiles()
}
for (const file of this.files.values()) {
yield file
}
}

static fromFileTree<T extends ColoredFileLeaf> (tree: FileTree<T>): FolderState<T> {
const folderState = new FolderState<T>()
for (const folder of tree.subTrees.keys()) {
Expand Down
2 changes: 2 additions & 0 deletions web/src/XGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { hashInt } from "./@utils/hashInt.ts";
export interface XLink extends Link {
isDir?: boolean
isPackage?: boolean
ignore?: boolean
}

export interface XNode extends Node, ColoredFileLeaf {
Expand All @@ -16,6 +17,7 @@ export interface XNode extends Node, ColoredFileLeaf {
z?: number
isDir?: boolean
isPackage?: boolean
ignore?: boolean
}

export interface XGraph extends Graph {
Expand Down

0 comments on commit 3ff0045

Please sign in to comment.