panel management

This commit is contained in:
Michael Zhang 2024-07-26 21:17:34 -05:00
parent ab8279db6e
commit 3958e1cc3d
9 changed files with 254 additions and 221 deletions

View file

@ -38,7 +38,7 @@ export async function loadApps(): Promise<Map<string, CustomApp>> {
const app = await loadApp(child); const app = await loadApp(child);
apps.set(app.name, app); apps.set(app.name, app);
} catch (e) { } catch (e) {
console.error("Error setting up " + child + ": " + e.message) console.error(`Error setting up ${child}: ${e.message}`);
} }
} }
} }

View file

@ -6,30 +6,29 @@ import "@fontsource/inter/700.css";
import "./global.scss"; import "./global.scss";
import "katex/dist/katex.min.css"; import "katex/dist/katex.min.css";
import { useEffect } from "react"; import { useEffect } from "react";
import NodeDisplay from "./components/NodeDisplay"; import PanelDisplay from "./components/PanelDisplay";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import TimeAgo from "javascript-time-ago"; import TimeAgo from "javascript-time-ago";
import en from "javascript-time-ago/locale/en"; import en from "javascript-time-ago/locale/en";
import Sidebar from "./components/Sidebar"; import Sidebar from "./components/Sidebar";
import { atom, useAtom, useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { OrderedSet } from "immutable";
import { PANORAMA_DAEMON_URL } from "./lib/constants"; import { PANORAMA_DAEMON_URL } from "./lib/constants";
import { panelsOpenedAtom, usePanelControls } from "./lib/panelManagement";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
TimeAgo.addDefaultLocale(en); TimeAgo.addDefaultLocale(en);
export const nodesOpenedAtom = atom<OrderedSet<string>>(OrderedSet<string>()); // export const nodesOpenedAtom = atom<OrderedSet<string>>(OrderedSet<string>());
function App() { function App() {
const nodesOpened = useAtomValue(nodesOpenedAtom); const panelsOpened = useAtomValue(panelsOpenedAtom);
const { openNode } = useNodeControls(); const { openNode } = usePanelControls();
// Open today's journal entry if it's not already opened // Open today's journal entry if it's not already opened
useEffect(() => { useEffect(() => {
(async () => { (async () => {
console.log("ndoes", nodesOpened); if (panelsOpened.size === 0) {
if (nodesOpened.size === 0) {
console.log("Opening today's entry."); console.log("Opening today's entry.");
const resp = await fetch( const resp = await fetch(
`${PANORAMA_DAEMON_URL}/journal/get_todays_journal_id`, `${PANORAMA_DAEMON_URL}/journal/get_todays_journal_id`,
@ -39,10 +38,10 @@ function App() {
openNode(data.node_id); openNode(data.node_id);
} }
})(); })();
}, [nodesOpened, openNode]); }, [openNode]);
const nodes = [...nodesOpened.reverse().values()].map((nodeId, idx) => ( const panels = [...panelsOpened.reverse().values()].map((panelInfo, idx) => (
<NodeDisplay idx={idx} key={nodeId} id={nodeId} /> <PanelDisplay idx={idx} key={JSON.stringify(panelInfo)} />
)); ));
return ( return (
@ -52,7 +51,7 @@ function App() {
<div className={styles.main}> <div className={styles.main}>
<Sidebar /> <Sidebar />
<div className={styles.nodeContainer}>{nodes}</div> <div className={styles.nodeContainer}>{panels}</div>
</div> </div>
</div> </div>
</QueryClientProvider> </QueryClientProvider>
@ -60,20 +59,3 @@ function App() {
} }
export default App; export default App;
export function useNodeControls() {
const [nodesOpened, setNodesOpened] = useAtom(nodesOpenedAtom);
return {
isOpen: (node_id: string) => nodesOpened.has(node_id),
toggleNode: (node_id: string) => {
if (nodesOpened.has(node_id)) setNodesOpened(nodesOpened.remove(node_id));
else setNodesOpened(nodesOpened.remove(node_id).add(node_id));
},
openNode: (node_id: string) => {
setNodesOpened(nodesOpened.remove(node_id).add(node_id));
},
closeNode: (node_id: string) => {
setNodesOpened(nodesOpened.remove(node_id));
},
};
}

View file

@ -5,7 +5,6 @@ import ListIcon from "@mui/icons-material/List";
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { sidebarExpandedAtom } from "./Sidebar"; import { sidebarExpandedAtom } from "./Sidebar";
import { useNodeControls } from "../App";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { getVersion } from "@tauri-apps/api/app"; import { getVersion } from "@tauri-apps/api/app";
@ -19,9 +18,10 @@ import {
useInteractions, useInteractions,
} from "@floating-ui/react"; } from "@floating-ui/react";
import { PANORAMA_DAEMON_URL } from "../lib/constants"; import { PANORAMA_DAEMON_URL } from "../lib/constants";
import { usePanelControls } from "../lib/panelManagement";
export default function Header() { export default function Header() {
const { openNode } = useNodeControls(); const { openNode } = usePanelControls();
const setSidebarExpanded = useSetAtom(sidebarExpandedAtom); const setSidebarExpanded = useSetAtom(sidebarExpandedAtom);
const versionData = useQuery({ const versionData = useQuery({
queryKey: ["appVersion"], queryKey: ["appVersion"],

View file

@ -5,85 +5,85 @@ import { getNode } from "../lib/getNode";
import FirstPageIcon from "@mui/icons-material/FirstPage"; import FirstPageIcon from "@mui/icons-material/FirstPage";
import MoreVertIcon from "@mui/icons-material/MoreVert"; import MoreVertIcon from "@mui/icons-material/MoreVert";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import { useNodeControls } from "../App"; import { usePanelControls } from "../lib/panelManagement";
export interface NodeDisplayProps { export interface NodeDisplayProps {
id: string; nodeId: string;
idx?: number | undefined; idx?: number | undefined;
} }
export default function NodeDisplay({ id, idx }: NodeDisplayProps) { export default function NodeDisplay({ nodeId, idx }: NodeDisplayProps) {
const query = useQuery({ const query = useQuery({
queryKey: ["fetchNode", id], queryKey: ["fetchNode", nodeId],
queryFn: getNode, queryFn: getNode,
}); });
const { isSuccess, status, data: nodeDescriptor } = query; const { isSuccess, status, data: nodeDescriptor } = query;
let Component = undefined; let Component = undefined;
let data = undefined; let data = undefined;
if (isSuccess) { if (isSuccess) {
Component = nodeDescriptor.render; Component = nodeDescriptor.render;
data = nodeDescriptor.data; data = nodeDescriptor.data;
} }
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.header}> <div className={styles.header}>
{isSuccess ? ( {isSuccess ? (
<NodeDisplayHeaderLoaded idx={idx} id={id} data={data} /> <NodeDisplayHeaderLoaded idx={idx} id={nodeId} data={data} />
) : ( ) : (
<> <>
ID {id} ({status}) ID {nodeId} ({status})
</> </>
)} )}
</div> </div>
<div className={styles.body}> <div className={styles.body}>
{Component && <Component id={id} data={data} />} {Component && <Component id={nodeId} data={data} />}
</div> </div>
<div className={styles.footer}>{id}</div> <div className={styles.footer}>{nodeId}</div>
</div> </div>
); );
} }
function NodeDisplayHeaderLoaded({ idx, id, data }) { function NodeDisplayHeaderLoaded({ idx, id, data }) {
const { openNode, closeNode } = useNodeControls(); const { openNode, closeNode } = usePanelControls();
const updatedAt = data.updated_at && Date.parse(data.updated_at); const updatedAt = data.updated_at && Date.parse(data.updated_at);
return ( return (
<> <>
{idx === 0 || ( {idx === 0 || (
<button <button
type="button" type="button"
onClick={() => openNode(id)} onClick={() => openNode(id)}
title="Move node to the left" title="Move node to the left"
> >
<FirstPageIcon fontSize="inherit" /> <FirstPageIcon fontSize="inherit" />
</button> </button>
)} )}
<span> <span>
Type {data.type}{" "} Type {data.type}{" "}
{updatedAt && ( {updatedAt && (
<> <>
&middot; Last updated <ReactTimeAgo date={updatedAt} /> &middot; Last updated <ReactTimeAgo date={updatedAt} />
</> </>
)} )}
</span> </span>
<div className="spacer" /> <div className="spacer" />
<button type="button"> <button type="button">
<MoreVertIcon fontSize="inherit" /> <MoreVertIcon fontSize="inherit" />
</button> </button>
<button <button
type="button" type="button"
className={styles.closeButton} className={styles.closeButton}
onClick={() => closeNode(id)} onClick={() => closeNode(id)}
> >
<CloseIcon fontSize="inherit" /> <CloseIcon fontSize="inherit" />
</button> </button>
</> </>
); );
} }

View file

@ -0,0 +1,9 @@
import type { PanelInfo } from "../lib/panelManagement";
export interface PanelDisplayProps {
info: PanelInfo;
}
export default function PanelDisplay({ info }: PanelDisplayProps) {
return <>Panel {JSON.stringify(info)}</>;
}

View file

@ -1,116 +1,116 @@
import styles from "./SearchBar.module.scss"; import styles from "./SearchBar.module.scss";
import { import {
FloatingOverlay, FloatingOverlay,
FloatingPortal, FloatingPortal,
autoUpdate, autoUpdate,
offset, offset,
useDismiss, useDismiss,
useFloating, useFloating,
useFocus, useFocus,
useInteractions, useInteractions,
} from "@floating-ui/react"; } from "@floating-ui/react";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { atom, useAtom, useSetAtom } from "jotai"; import { atom, useAtom, useSetAtom } from "jotai";
import { useNodeControls } from "../App";
import { useDebounce, useDebouncedCallback } from "use-debounce"; import { useDebounce, useDebouncedCallback } from "use-debounce";
import { usePanelControls } from "../lib/panelManagement";
const searchQueryAtom = atom(""); const searchQueryAtom = atom("");
const showMenuAtom = atom(false); const showMenuAtom = atom(false);
export default function SearchBar() { export default function SearchBar() {
const [showMenu, setShowMenu] = useAtom(showMenuAtom); const [showMenu, setShowMenu] = useAtom(showMenuAtom);
const [searchQuery, setSearchQuery] = useAtom(searchQueryAtom); const [searchQuery, setSearchQuery] = useAtom(searchQueryAtom);
const [searchResults, setSearchResults] = useState([]); const [searchResults, setSearchResults] = useState([]);
const { refs, context, floatingStyles } = useFloating({ const { refs, context, floatingStyles } = useFloating({
placement: "bottom-start", placement: "bottom-start",
open: showMenu, open: showMenu,
onOpenChange: setShowMenu, onOpenChange: setShowMenu,
whileElementsMounted: autoUpdate, whileElementsMounted: autoUpdate,
middleware: [offset(10)], middleware: [offset(10)],
}); });
const focus = useFocus(context); const focus = useFocus(context);
const { getReferenceProps, getFloatingProps } = useInteractions([ const { getReferenceProps, getFloatingProps } = useInteractions([
focus, focus,
useDismiss(context), useDismiss(context),
]); ]);
const performSearch = useCallback(() => { const performSearch = useCallback(() => {
const trimmed = searchQuery.trim(); const trimmed = searchQuery.trim();
if (trimmed === "") return; if (trimmed === "") return;
(async () => { (async () => {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.set("query", trimmed); params.set("query", trimmed);
const resp = await fetch( const resp = await fetch(
`http://localhost:5195/node/search?${params.toString()}`, `http://localhost:5195/node/search?${params.toString()}`,
); );
const data = await resp.json(); const data = await resp.json();
setSearchResults(data.results); setSearchResults(data.results);
})(); })();
}, [searchQuery]); }, [searchQuery]);
return ( return (
<> <>
<div> <div>
<input <input
className={styles.entry} className={styles.entry}
type="text" type="text"
placeholder="Search..." placeholder="Search..."
onFocus={() => setShowMenu(true)} onFocus={() => setShowMenu(true)}
ref={refs.setReference} ref={refs.setReference}
value={searchQuery} value={searchQuery}
onChange={(evt) => { onChange={(evt) => {
setSearchQuery(evt.target.value); setSearchQuery(evt.target.value);
if (evt.target.value) performSearch(); if (evt.target.value) performSearch();
else setSearchResults([]); else setSearchResults([]);
}} }}
{...getReferenceProps()} {...getReferenceProps()}
/> />
</div> </div>
{showMenu && ( {showMenu && (
<FloatingPortal> <FloatingPortal>
<FloatingOverlay> <FloatingOverlay>
<div <div
ref={refs.setFloating} ref={refs.setFloating}
className={styles.menu} className={styles.menu}
style={{ ...floatingStyles }} style={{ ...floatingStyles }}
{...getFloatingProps()} {...getFloatingProps()}
> >
<SearchMenu results={searchResults} /> <SearchMenu results={searchResults} />
</div> </div>
</FloatingOverlay> </FloatingOverlay>
</FloatingPortal> </FloatingPortal>
)} )}
</> </>
); );
} }
function SearchMenu({ results }) { function SearchMenu({ results }) {
const setSearchQuery = useSetAtom(searchQueryAtom); const setSearchQuery = useSetAtom(searchQueryAtom);
const setShowMenu = useSetAtom(showMenuAtom); const setShowMenu = useSetAtom(showMenuAtom);
const { openNode } = useNodeControls(); const { openNode } = usePanelControls();
return ( return (
<div className={styles.searchResults}> <div className={styles.searchResults}>
{results.map((result) => { {results.map((result) => {
return ( return (
<button <button
type="button" type="button"
key={result.node_id} key={result.node_id}
className={styles.searchResult} className={styles.searchResult}
onClick={() => { onClick={() => {
setSearchQuery(""); setSearchQuery("");
setShowMenu(false); setShowMenu(false);
openNode(result.node_id); openNode(result.node_id);
}} }}
> >
{/* <div className={styles.title}>{result.title}</div> */} {/* <div className={styles.title}>{result.title}</div> */}
<div className={styles.subtitle}>{JSON.stringify(result)}</div> <div className={styles.subtitle}>{JSON.stringify(result)}</div>
</button> </button>
); );
})} })}
</div> </div>
); );
} }

View file

@ -3,39 +3,39 @@ import styles from "./Sidebar.module.scss";
import classNames from "classnames"; import classNames from "classnames";
import EmailIcon from "@mui/icons-material/Email"; import EmailIcon from "@mui/icons-material/Email";
import SettingsIcon from "@mui/icons-material/Settings"; import SettingsIcon from "@mui/icons-material/Settings";
import { useNodeControls } from "../App"; import { usePanelControls } from "../lib/panelManagement";
export const sidebarExpandedAtom = atom(false); export const sidebarExpandedAtom = atom(false);
export default function Sidebar() { export default function Sidebar() {
const sidebarExpanded = useAtomValue(sidebarExpandedAtom); const sidebarExpanded = useAtomValue(sidebarExpandedAtom);
const { toggleNode, isOpen } = useNodeControls(); const { toggleNodePanel, isNodeOpen } = usePanelControls();
return ( return (
<div <div
className={classNames( className={classNames(
styles.sidebar, styles.sidebar,
sidebarExpanded ? styles.expanded : styles.collapsed, sidebarExpanded ? styles.expanded : styles.collapsed,
)} )}
> >
<button <button
type="button" type="button"
className={classNames( className={classNames(
styles.item, styles.item,
isOpen("panorama/mail") && styles.active, isNodeOpen("panorama/mail") && styles.active,
)} )}
onClick={() => toggleNode("panorama/mail")} onClick={() => toggleNodePanel("panorama/mail")}
> >
<EmailIcon /> <EmailIcon />
<span className={styles.label}>Email</span> <span className={styles.label}>Email</span>
</button> </button>
<div className="spacer" /> <div className="spacer" />
<div className={styles.item}> <div className={styles.item}>
<SettingsIcon /> <SettingsIcon />
<span className={styles.label}>Settings</span> <span className={styles.label}>Settings</span>
</div> </div>
</div> </div>
); );
} }

View file

@ -0,0 +1,13 @@
export interface QueryTableProps {}
export default function QueryTable({}: QueryTableProps) {
return (
<div>
<div>
<input placeholder="Search..." />
</div>
<div></div>
</div>
);
}

View file

@ -0,0 +1,29 @@
import { OrderedSet } from "immutable";
import { atom, useAtom } from "jotai";
export type PanelInfo = { type: "node"; nodeId: string };
export const panelsOpenedAtom = atom<OrderedSet<PanelInfo>>(
OrderedSet<PanelInfo>(),
);
export function usePanelControls() {
const [nodesOpened, setNodesOpened] = useAtom(panelsOpenedAtom);
return {
isNodeOpen: (nodeId: string) => nodesOpened.has({ type: "node", nodeId }),
toggleNodePanel: (nodeId: string) => {
const info: PanelInfo = { type: "node", nodeId };
if (nodesOpened.has(info)) setNodesOpened(nodesOpened.remove(info));
else setNodesOpened(nodesOpened.remove(info).add(info));
},
openNode: (nodeId: string) => {
const info: PanelInfo = { type: "node", nodeId };
setNodesOpened(nodesOpened.remove(info).add(info));
},
closeNode: (nodeId: string) => {
const info: PanelInfo = { type: "node", nodeId };
setNodesOpened(nodesOpened.remove(info));
},
};
}