panel management
This commit is contained in:
parent
ab8279db6e
commit
3958e1cc3d
9 changed files with 254 additions and 221 deletions
|
@ -38,7 +38,7 @@ export async function loadApps(): Promise<Map<string, CustomApp>> {
|
|||
const app = await loadApp(child);
|
||||
apps.set(app.name, app);
|
||||
} catch (e) {
|
||||
console.error("Error setting up " + child + ": " + e.message)
|
||||
console.error(`Error setting up ${child}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,30 +6,29 @@ import "@fontsource/inter/700.css";
|
|||
import "./global.scss";
|
||||
import "katex/dist/katex.min.css";
|
||||
import { useEffect } from "react";
|
||||
import NodeDisplay from "./components/NodeDisplay";
|
||||
import PanelDisplay from "./components/PanelDisplay";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import TimeAgo from "javascript-time-ago";
|
||||
import en from "javascript-time-ago/locale/en";
|
||||
import Sidebar from "./components/Sidebar";
|
||||
import { atom, useAtom, useAtomValue } from "jotai";
|
||||
import { OrderedSet } from "immutable";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { PANORAMA_DAEMON_URL } from "./lib/constants";
|
||||
import { panelsOpenedAtom, usePanelControls } from "./lib/panelManagement";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
TimeAgo.addDefaultLocale(en);
|
||||
|
||||
export const nodesOpenedAtom = atom<OrderedSet<string>>(OrderedSet<string>());
|
||||
// export const nodesOpenedAtom = atom<OrderedSet<string>>(OrderedSet<string>());
|
||||
|
||||
function App() {
|
||||
const nodesOpened = useAtomValue(nodesOpenedAtom);
|
||||
const { openNode } = useNodeControls();
|
||||
const panelsOpened = useAtomValue(panelsOpenedAtom);
|
||||
const { openNode } = usePanelControls();
|
||||
|
||||
// Open today's journal entry if it's not already opened
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
console.log("ndoes", nodesOpened);
|
||||
if (nodesOpened.size === 0) {
|
||||
if (panelsOpened.size === 0) {
|
||||
console.log("Opening today's entry.");
|
||||
const resp = await fetch(
|
||||
`${PANORAMA_DAEMON_URL}/journal/get_todays_journal_id`,
|
||||
|
@ -39,10 +38,10 @@ function App() {
|
|||
openNode(data.node_id);
|
||||
}
|
||||
})();
|
||||
}, [nodesOpened, openNode]);
|
||||
}, [openNode]);
|
||||
|
||||
const nodes = [...nodesOpened.reverse().values()].map((nodeId, idx) => (
|
||||
<NodeDisplay idx={idx} key={nodeId} id={nodeId} />
|
||||
const panels = [...panelsOpened.reverse().values()].map((panelInfo, idx) => (
|
||||
<PanelDisplay idx={idx} key={JSON.stringify(panelInfo)} />
|
||||
));
|
||||
|
||||
return (
|
||||
|
@ -52,7 +51,7 @@ function App() {
|
|||
|
||||
<div className={styles.main}>
|
||||
<Sidebar />
|
||||
<div className={styles.nodeContainer}>{nodes}</div>
|
||||
<div className={styles.nodeContainer}>{panels}</div>
|
||||
</div>
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
|
@ -60,20 +59,3 @@ function 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));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import ListIcon from "@mui/icons-material/List";
|
|||
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { sidebarExpandedAtom } from "./Sidebar";
|
||||
import { useNodeControls } from "../App";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
|
@ -19,9 +18,10 @@ import {
|
|||
useInteractions,
|
||||
} from "@floating-ui/react";
|
||||
import { PANORAMA_DAEMON_URL } from "../lib/constants";
|
||||
import { usePanelControls } from "../lib/panelManagement";
|
||||
|
||||
export default function Header() {
|
||||
const { openNode } = useNodeControls();
|
||||
const { openNode } = usePanelControls();
|
||||
const setSidebarExpanded = useSetAtom(sidebarExpandedAtom);
|
||||
const versionData = useQuery({
|
||||
queryKey: ["appVersion"],
|
||||
|
|
|
@ -5,85 +5,85 @@ import { getNode } from "../lib/getNode";
|
|||
import FirstPageIcon from "@mui/icons-material/FirstPage";
|
||||
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { useNodeControls } from "../App";
|
||||
import { usePanelControls } from "../lib/panelManagement";
|
||||
|
||||
export interface NodeDisplayProps {
|
||||
id: string;
|
||||
idx?: number | undefined;
|
||||
nodeId: string;
|
||||
idx?: number | undefined;
|
||||
}
|
||||
|
||||
export default function NodeDisplay({ id, idx }: NodeDisplayProps) {
|
||||
const query = useQuery({
|
||||
queryKey: ["fetchNode", id],
|
||||
queryFn: getNode,
|
||||
});
|
||||
export default function NodeDisplay({ nodeId, idx }: NodeDisplayProps) {
|
||||
const query = useQuery({
|
||||
queryKey: ["fetchNode", nodeId],
|
||||
queryFn: getNode,
|
||||
});
|
||||
|
||||
const { isSuccess, status, data: nodeDescriptor } = query;
|
||||
const { isSuccess, status, data: nodeDescriptor } = query;
|
||||
|
||||
let Component = undefined;
|
||||
let data = undefined;
|
||||
if (isSuccess) {
|
||||
Component = nodeDescriptor.render;
|
||||
data = nodeDescriptor.data;
|
||||
}
|
||||
let Component = undefined;
|
||||
let data = undefined;
|
||||
if (isSuccess) {
|
||||
Component = nodeDescriptor.render;
|
||||
data = nodeDescriptor.data;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
{isSuccess ? (
|
||||
<NodeDisplayHeaderLoaded idx={idx} id={id} data={data} />
|
||||
) : (
|
||||
<>
|
||||
ID {id} ({status})
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
{isSuccess ? (
|
||||
<NodeDisplayHeaderLoaded idx={idx} id={nodeId} data={data} />
|
||||
) : (
|
||||
<>
|
||||
ID {nodeId} ({status})
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.body}>
|
||||
{Component && <Component id={id} data={data} />}
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
{Component && <Component id={nodeId} data={data} />}
|
||||
</div>
|
||||
|
||||
<div className={styles.footer}>{id}</div>
|
||||
</div>
|
||||
);
|
||||
<div className={styles.footer}>{nodeId}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NodeDisplayHeaderLoaded({ idx, id, data }) {
|
||||
const { openNode, closeNode } = useNodeControls();
|
||||
const updatedAt = data.updated_at && Date.parse(data.updated_at);
|
||||
const { openNode, closeNode } = usePanelControls();
|
||||
const updatedAt = data.updated_at && Date.parse(data.updated_at);
|
||||
|
||||
return (
|
||||
<>
|
||||
{idx === 0 || (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openNode(id)}
|
||||
title="Move node to the left"
|
||||
>
|
||||
<FirstPageIcon fontSize="inherit" />
|
||||
</button>
|
||||
)}
|
||||
<span>
|
||||
Type {data.type}{" "}
|
||||
{updatedAt && (
|
||||
<>
|
||||
· Last updated <ReactTimeAgo date={updatedAt} />
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
<div className="spacer" />
|
||||
return (
|
||||
<>
|
||||
{idx === 0 || (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openNode(id)}
|
||||
title="Move node to the left"
|
||||
>
|
||||
<FirstPageIcon fontSize="inherit" />
|
||||
</button>
|
||||
)}
|
||||
<span>
|
||||
Type {data.type}{" "}
|
||||
{updatedAt && (
|
||||
<>
|
||||
· Last updated <ReactTimeAgo date={updatedAt} />
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
<div className="spacer" />
|
||||
|
||||
<button type="button">
|
||||
<MoreVertIcon fontSize="inherit" />
|
||||
</button>
|
||||
<button type="button">
|
||||
<MoreVertIcon fontSize="inherit" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={styles.closeButton}
|
||||
onClick={() => closeNode(id)}
|
||||
>
|
||||
<CloseIcon fontSize="inherit" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
<button
|
||||
type="button"
|
||||
className={styles.closeButton}
|
||||
onClick={() => closeNode(id)}
|
||||
>
|
||||
<CloseIcon fontSize="inherit" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
9
ui/src/components/PanelDisplay.tsx
Normal file
9
ui/src/components/PanelDisplay.tsx
Normal 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)}</>;
|
||||
}
|
|
@ -1,116 +1,116 @@
|
|||
import styles from "./SearchBar.module.scss";
|
||||
import {
|
||||
FloatingOverlay,
|
||||
FloatingPortal,
|
||||
autoUpdate,
|
||||
offset,
|
||||
useDismiss,
|
||||
useFloating,
|
||||
useFocus,
|
||||
useInteractions,
|
||||
FloatingOverlay,
|
||||
FloatingPortal,
|
||||
autoUpdate,
|
||||
offset,
|
||||
useDismiss,
|
||||
useFloating,
|
||||
useFocus,
|
||||
useInteractions,
|
||||
} from "@floating-ui/react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { atom, useAtom, useSetAtom } from "jotai";
|
||||
import { useNodeControls } from "../App";
|
||||
import { useDebounce, useDebouncedCallback } from "use-debounce";
|
||||
import { usePanelControls } from "../lib/panelManagement";
|
||||
|
||||
const searchQueryAtom = atom("");
|
||||
const showMenuAtom = atom(false);
|
||||
|
||||
export default function SearchBar() {
|
||||
const [showMenu, setShowMenu] = useAtom(showMenuAtom);
|
||||
const [searchQuery, setSearchQuery] = useAtom(searchQueryAtom);
|
||||
const [searchResults, setSearchResults] = useState([]);
|
||||
const [showMenu, setShowMenu] = useAtom(showMenuAtom);
|
||||
const [searchQuery, setSearchQuery] = useAtom(searchQueryAtom);
|
||||
const [searchResults, setSearchResults] = useState([]);
|
||||
|
||||
const { refs, context, floatingStyles } = useFloating({
|
||||
placement: "bottom-start",
|
||||
open: showMenu,
|
||||
onOpenChange: setShowMenu,
|
||||
whileElementsMounted: autoUpdate,
|
||||
middleware: [offset(10)],
|
||||
});
|
||||
const focus = useFocus(context);
|
||||
const { getReferenceProps, getFloatingProps } = useInteractions([
|
||||
focus,
|
||||
useDismiss(context),
|
||||
]);
|
||||
const { refs, context, floatingStyles } = useFloating({
|
||||
placement: "bottom-start",
|
||||
open: showMenu,
|
||||
onOpenChange: setShowMenu,
|
||||
whileElementsMounted: autoUpdate,
|
||||
middleware: [offset(10)],
|
||||
});
|
||||
const focus = useFocus(context);
|
||||
const { getReferenceProps, getFloatingProps } = useInteractions([
|
||||
focus,
|
||||
useDismiss(context),
|
||||
]);
|
||||
|
||||
const performSearch = useCallback(() => {
|
||||
const trimmed = searchQuery.trim();
|
||||
if (trimmed === "") return;
|
||||
const performSearch = useCallback(() => {
|
||||
const trimmed = searchQuery.trim();
|
||||
if (trimmed === "") return;
|
||||
|
||||
(async () => {
|
||||
const params = new URLSearchParams();
|
||||
params.set("query", trimmed);
|
||||
const resp = await fetch(
|
||||
`http://localhost:5195/node/search?${params.toString()}`,
|
||||
);
|
||||
const data = await resp.json();
|
||||
setSearchResults(data.results);
|
||||
})();
|
||||
}, [searchQuery]);
|
||||
(async () => {
|
||||
const params = new URLSearchParams();
|
||||
params.set("query", trimmed);
|
||||
const resp = await fetch(
|
||||
`http://localhost:5195/node/search?${params.toString()}`,
|
||||
);
|
||||
const data = await resp.json();
|
||||
setSearchResults(data.results);
|
||||
})();
|
||||
}, [searchQuery]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<input
|
||||
className={styles.entry}
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
onFocus={() => setShowMenu(true)}
|
||||
ref={refs.setReference}
|
||||
value={searchQuery}
|
||||
onChange={(evt) => {
|
||||
setSearchQuery(evt.target.value);
|
||||
if (evt.target.value) performSearch();
|
||||
else setSearchResults([]);
|
||||
}}
|
||||
{...getReferenceProps()}
|
||||
/>
|
||||
</div>
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<input
|
||||
className={styles.entry}
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
onFocus={() => setShowMenu(true)}
|
||||
ref={refs.setReference}
|
||||
value={searchQuery}
|
||||
onChange={(evt) => {
|
||||
setSearchQuery(evt.target.value);
|
||||
if (evt.target.value) performSearch();
|
||||
else setSearchResults([]);
|
||||
}}
|
||||
{...getReferenceProps()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showMenu && (
|
||||
<FloatingPortal>
|
||||
<FloatingOverlay>
|
||||
<div
|
||||
ref={refs.setFloating}
|
||||
className={styles.menu}
|
||||
style={{ ...floatingStyles }}
|
||||
{...getFloatingProps()}
|
||||
>
|
||||
<SearchMenu results={searchResults} />
|
||||
</div>
|
||||
</FloatingOverlay>
|
||||
</FloatingPortal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
{showMenu && (
|
||||
<FloatingPortal>
|
||||
<FloatingOverlay>
|
||||
<div
|
||||
ref={refs.setFloating}
|
||||
className={styles.menu}
|
||||
style={{ ...floatingStyles }}
|
||||
{...getFloatingProps()}
|
||||
>
|
||||
<SearchMenu results={searchResults} />
|
||||
</div>
|
||||
</FloatingOverlay>
|
||||
</FloatingPortal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SearchMenu({ results }) {
|
||||
const setSearchQuery = useSetAtom(searchQueryAtom);
|
||||
const setShowMenu = useSetAtom(showMenuAtom);
|
||||
const { openNode } = useNodeControls();
|
||||
const setSearchQuery = useSetAtom(searchQueryAtom);
|
||||
const setShowMenu = useSetAtom(showMenuAtom);
|
||||
const { openNode } = usePanelControls();
|
||||
|
||||
return (
|
||||
<div className={styles.searchResults}>
|
||||
{results.map((result) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={result.node_id}
|
||||
className={styles.searchResult}
|
||||
onClick={() => {
|
||||
setSearchQuery("");
|
||||
setShowMenu(false);
|
||||
openNode(result.node_id);
|
||||
}}
|
||||
>
|
||||
{/* <div className={styles.title}>{result.title}</div> */}
|
||||
<div className={styles.subtitle}>{JSON.stringify(result)}</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.searchResults}>
|
||||
{results.map((result) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={result.node_id}
|
||||
className={styles.searchResult}
|
||||
onClick={() => {
|
||||
setSearchQuery("");
|
||||
setShowMenu(false);
|
||||
openNode(result.node_id);
|
||||
}}
|
||||
>
|
||||
{/* <div className={styles.title}>{result.title}</div> */}
|
||||
<div className={styles.subtitle}>{JSON.stringify(result)}</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,39 +3,39 @@ import styles from "./Sidebar.module.scss";
|
|||
import classNames from "classnames";
|
||||
import EmailIcon from "@mui/icons-material/Email";
|
||||
import SettingsIcon from "@mui/icons-material/Settings";
|
||||
import { useNodeControls } from "../App";
|
||||
import { usePanelControls } from "../lib/panelManagement";
|
||||
|
||||
export const sidebarExpandedAtom = atom(false);
|
||||
|
||||
export default function Sidebar() {
|
||||
const sidebarExpanded = useAtomValue(sidebarExpandedAtom);
|
||||
const { toggleNode, isOpen } = useNodeControls();
|
||||
const sidebarExpanded = useAtomValue(sidebarExpandedAtom);
|
||||
const { toggleNodePanel, isNodeOpen } = usePanelControls();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.sidebar,
|
||||
sidebarExpanded ? styles.expanded : styles.collapsed,
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
styles.item,
|
||||
isOpen("panorama/mail") && styles.active,
|
||||
)}
|
||||
onClick={() => toggleNode("panorama/mail")}
|
||||
>
|
||||
<EmailIcon />
|
||||
<span className={styles.label}>Email</span>
|
||||
</button>
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.sidebar,
|
||||
sidebarExpanded ? styles.expanded : styles.collapsed,
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
styles.item,
|
||||
isNodeOpen("panorama/mail") && styles.active,
|
||||
)}
|
||||
onClick={() => toggleNodePanel("panorama/mail")}
|
||||
>
|
||||
<EmailIcon />
|
||||
<span className={styles.label}>Email</span>
|
||||
</button>
|
||||
|
||||
<div className="spacer" />
|
||||
<div className="spacer" />
|
||||
|
||||
<div className={styles.item}>
|
||||
<SettingsIcon />
|
||||
<span className={styles.label}>Settings</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className={styles.item}>
|
||||
<SettingsIcon />
|
||||
<span className={styles.label}>Settings</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
13
ui/src/components/nodes/Query.tsx
Normal file
13
ui/src/components/nodes/Query.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
export interface QueryTableProps {}
|
||||
|
||||
export default function QueryTable({}: QueryTableProps) {
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<input placeholder="Search..." />
|
||||
</div>
|
||||
|
||||
<div></div>
|
||||
</div>
|
||||
);
|
||||
}
|
29
ui/src/lib/panelManagement.ts
Normal file
29
ui/src/lib/panelManagement.ts
Normal 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));
|
||||
},
|
||||
};
|
||||
}
|
Loading…
Reference in a new issue