diff options
| author | Ellie Huxtable <ellie@atuin.sh> | 2024-07-15 19:12:01 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-07-15 19:12:01 +0100 |
| commit | 7eb985b616c12aed261fbef74a47c5a928c03e61 (patch) | |
| tree | fe3088dc687c640176bf376b4ca8baf2088bd69f /ui/src/components/runbooks | |
| parent | chore(deps): bump cachix/install-nix-action from 20 to 27 (#2268) (diff) | |
| download | atuin-7eb985b616c12aed261fbef74a47c5a928c03e61.zip | |
feat(gui): add runbook list, ability to create and delete, sql storage (#2282)
* wip
* saving works :))
* functioning delete button
* persist selection properly
Diffstat (limited to 'ui/src/components/runbooks')
| -rw-r--r-- | ui/src/components/runbooks/List.tsx | 120 | ||||
| -rw-r--r-- | ui/src/components/runbooks/editor/Editor.tsx | 94 | ||||
| -rw-r--r-- | ui/src/components/runbooks/editor/blocks/RunBlock/index.tsx | 23 |
3 files changed, 212 insertions, 25 deletions
diff --git a/ui/src/components/runbooks/List.tsx b/ui/src/components/runbooks/List.tsx new file mode 100644 index 00000000..be6e84f5 --- /dev/null +++ b/ui/src/components/runbooks/List.tsx @@ -0,0 +1,120 @@ +import { useEffect, useState } from "react"; +import { + Input, + Button, + ButtonGroup, + Card, + CardBody, + CardHeader, + Divider, + Tooltip, + Listbox, + ListboxItem, + Dropdown, + DropdownTrigger, + DropdownMenu, + DropdownItem, +} from "@nextui-org/react"; + +import { EllipsisVerticalIcon } from "lucide-react"; + +import { DateTime } from "luxon"; + +import { NotebookPenIcon } from "lucide-react"; +import Runbook from "@/state/runbooks/runbook"; +import { useStore } from "@/state/store"; + +const NoteSidebar = () => { + const runbooks = useStore((state) => state.runbooks); + const refreshRunbooks = useStore((state) => state.refreshRunbooks); + + const currentRunbook = useStore((state) => state.currentRunbook); + const setCurrentRunbook = useStore((state) => state.setCurrentRunbook); + + useEffect(() => { + refreshRunbooks(); + }, []); + + return ( + <div className="min-w-48 h-screen flex flex-col border-r-1"> + <div className="overflow-y-auto flex-grow"> + <Listbox + hideSelectedIcon + items={runbooks} + variant="flat" + aria-label="Runbook list" + selectionMode="single" + selectedKeys={[currentRunbook]} + itemClasses={{ base: "data-[selected=true]:bg-gray-200" }} + topContent={ + <ButtonGroup> + <Tooltip showArrow content="New Runbook" closeDelay={50}> + <Button + isIconOnly + aria-label="New note" + variant="light" + size="sm" + onPress={async () => { + let runbook = await Runbook.create(); + setCurrentRunbook(runbook.id); + refreshRunbooks(); + }} + > + <NotebookPenIcon className="p-[0.15rem]" /> + </Button> + </Tooltip> + </ButtonGroup> + } + > + {(runbook) => ( + <ListboxItem + key={runbook.id} + onPress={() => { + setCurrentRunbook(runbook.id); + }} + textValue={runbook.name || "Untitled"} + endContent={ + <Dropdown> + <DropdownTrigger className="bg-transparent"> + <Button isIconOnly> + <EllipsisVerticalIcon + size="16px" + className="bg-transparent" + /> + </Button> + </DropdownTrigger> + <DropdownMenu aria-label="Dynamic Actions"> + <DropdownItem + key={"delete"} + color="danger" + className="text-danger" + onPress={async () => { + await Runbook.delete(runbook.id); + refreshRunbooks(); + }} + > + Delete + </DropdownItem> + </DropdownMenu> + </Dropdown> + } + > + <div className="flex flex-col"> + <div className="text-md">{runbook.name || "Untitled"}</div> + <div className="text-xs text-gray-500"> + <em> + {DateTime.fromJSDate(runbook.updated).toLocaleString( + DateTime.DATETIME_SIMPLE, + )} + </em> + </div> + </div> + </ListboxItem> + )} + </Listbox> + </div> + </div> + ); +}; + +export default NoteSidebar; diff --git a/ui/src/components/runbooks/editor/Editor.tsx b/ui/src/components/runbooks/editor/Editor.tsx index 3f05d9f3..8ed52660 100644 --- a/ui/src/components/runbooks/editor/Editor.tsx +++ b/ui/src/components/runbooks/editor/Editor.tsx @@ -1,13 +1,17 @@ +import { useEffect, useMemo, useState } from "react"; + import "@blocknote/core/fonts/inter.css"; import "@blocknote/mantine/style.css"; import "./index.css"; import { BlockNoteSchema, + BlockNoteEditor, defaultBlockSpecs, filterSuggestionItems, insertOrUpdateBlock, } from "@blocknote/core"; + import "@blocknote/core/fonts/inter.css"; import { @@ -21,9 +25,12 @@ import { import { BlockNoteView } from "@blocknote/mantine"; import { Code } from "lucide-react"; +import { useDebounceCallback } from "usehooks-ts"; import RunBlock from "@/components/runbooks/editor/blocks/RunBlock"; import { DeleteBlock } from "@/components/runbooks/editor/ui/DeleteBlockButton"; +import { useStore } from "@/state/store"; +import Runbook from "@/state/runbooks/runbook"; // Our schema with block specs, which contain the configs and implementations for blocks // that we want our editor to use. @@ -51,26 +58,79 @@ const insertRun = (editor: typeof schema.BlockNoteEditor) => ({ }); export default function Editor() { - // Creates a new editor instance. - const editor = useCreateBlockNote({ - schema, - initialContent: [ - { - type: "heading", - content: "Atuin runbooks", - id: "foo", - }, - { - type: "run", - id: "bar", - }, - ], - }); + const runbookId = useStore((store) => store.currentRunbook); + const refreshRunbooks = useStore((store) => store.refreshRunbooks); + let [runbook, setRunbook] = useState<Runbook | null>(null); + + useEffect(() => { + if (!runbookId) return; + + const fetchRunbook = async () => { + let rb = await Runbook.load(runbookId); + + setRunbook(rb); + }; + + fetchRunbook(); + }, [runbookId]); + + const editor = useMemo(() => { + if (!runbook) { + return undefined; + } + + if (runbook.content) { + return BlockNoteEditor.create({ + initialContent: JSON.parse(runbook.content), + schema, + }); + } + + return BlockNoteEditor.create({ schema }); + }, [runbook]); + + const onChange = async () => { + if (!runbook) return; + + console.log("saved!"); + runbook.name = fetchName(); + runbook.content = JSON.stringify(editor.document); + + await runbook.save(); + await refreshRunbooks(); + }; + + const debouncedOnChange = useDebounceCallback(onChange, 1000); + + const fetchName = (): String => { + // Infer the title from the first text block + + let blocks = editor.document; + for (const block of blocks) { + if (block.type == "heading" || block.type == "paragraph") { + if (block.content.length == 0) continue; + if (block.content[0].text.length == 0) continue; + + return block.content[0].text; + } + } + + return "Untitled"; + }; + + if (editor === undefined) { + return "Loading content..."; + } // Renders the editor instance. return ( - <div> - <BlockNoteView editor={editor} slashMenu={false} sideMenu={false}> + <div className="p-4 w-full"> + <BlockNoteView + editor={editor} + slashMenu={false} + sideMenu={false} + onChange={debouncedOnChange} + > <SuggestionMenuController triggerCharacter={"/"} getItems={async (query) => diff --git a/ui/src/components/runbooks/editor/blocks/RunBlock/index.tsx b/ui/src/components/runbooks/editor/blocks/RunBlock/index.tsx index 78928876..9b2fe515 100644 --- a/ui/src/components/runbooks/editor/blocks/RunBlock/index.tsx +++ b/ui/src/components/runbooks/editor/blocks/RunBlock/index.tsx @@ -23,17 +23,20 @@ interface RunBlockProps { isEditable: boolean; } -const RunBlock = ({ onPlay, id, code, isEditable }: RunBlockProps) => { +const RunBlock = ({ + onChange, + onPlay, + id, + code, + isEditable, +}: RunBlockProps) => { + console.log(code); const [isRunning, setIsRunning] = useState(false); const [showTerminal, setShowTerminal] = useState(false); const [value, setValue] = useState<String>(code); const [pty, setPty] = useState<string | null>(null); - const onChange = (val: any) => { - setValue(val); - }; - const handleToggle = async (event: any) => { event.stopPropagation(); @@ -88,7 +91,10 @@ const RunBlock = ({ onPlay, id, code, isEditable }: RunBlockProps) => { editable={isEditable} width="100%" autoFocus - onChange={onChange} + onChange={(val) => { + setValue(val); + onChange(val); + }} extensions={[...extensions(), langs.shell()]} basicSetup={false} /> @@ -123,14 +129,15 @@ export default createReactBlockSpec( editor.updateBlock(block, { props: { ...block.props, code: val }, }); + console.log(block.props); }; return ( <RunBlock onChange={onInputChange} id={block?.id} - code={code} - type={type} + code={block.props.code} + type={block.props.type} isEditable={editor.isEditable} /> ); |
