From 808138de633e410c1d3867d4fb7cb74967647605 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Tue, 30 Jul 2024 16:54:10 +0100 Subject: chore: remove ui directory (#2329) This is still in development, but rather than clutter the commit history and issues with an unreleased project I've split the UI into its own repo. Once ready for release, I'll either merge the ui code back in, or just make the repo public. --- ui/src/components/runbooks/editor/Editor.tsx | 200 ------------------ .../runbooks/editor/blocks/Directory/index.tsx | 89 -------- .../runbooks/editor/blocks/Run/extensions.ts | 158 -------------- .../runbooks/editor/blocks/Run/index.css | 9 - .../runbooks/editor/blocks/Run/index.tsx | 229 --------------------- .../runbooks/editor/blocks/Run/terminal.tsx | 113 ---------- ui/src/components/runbooks/editor/index.css | 7 - .../runbooks/editor/ui/DeleteBlockButton.tsx | 28 --- 8 files changed, 833 deletions(-) delete mode 100644 ui/src/components/runbooks/editor/Editor.tsx delete mode 100644 ui/src/components/runbooks/editor/blocks/Directory/index.tsx delete mode 100644 ui/src/components/runbooks/editor/blocks/Run/extensions.ts delete mode 100644 ui/src/components/runbooks/editor/blocks/Run/index.css delete mode 100644 ui/src/components/runbooks/editor/blocks/Run/index.tsx delete mode 100644 ui/src/components/runbooks/editor/blocks/Run/terminal.tsx delete mode 100644 ui/src/components/runbooks/editor/index.css delete mode 100644 ui/src/components/runbooks/editor/ui/DeleteBlockButton.tsx (limited to 'ui/src/components/runbooks/editor') diff --git a/ui/src/components/runbooks/editor/Editor.tsx b/ui/src/components/runbooks/editor/Editor.tsx deleted file mode 100644 index 6b0522f5..00000000 --- a/ui/src/components/runbooks/editor/Editor.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; - -import "./index.css"; - -import { Spinner } from "@nextui-org/react"; - -// Errors, but it all works fine and is there. Maybe missing ts defs? -// I'll figure it out later -import { - // @ts-ignore - BlockNoteSchema, - // @ts-ignore - BlockNoteEditor, - // @ts-ignore - defaultBlockSpecs, - // @ts-ignore - filterSuggestionItems, - // @ts-ignore - insertOrUpdateBlock, -} from "@blocknote/core"; - -import { - //@ts-ignore - SuggestionMenuController, - // @ts-ignore - AddBlockButton, - // @ts-ignore - getDefaultReactSlashMenuItems, - // @ts-ignore - SideMenu, - // @ts-ignore - SideMenuController, -} from "@blocknote/react"; -import { BlockNoteView } from "@blocknote/mantine"; - -import "@blocknote/core/fonts/inter.css"; -import "@blocknote/mantine/style.css"; - -import { CodeIcon, FolderOpenIcon } from "lucide-react"; -import { useDebounceCallback } from "usehooks-ts"; - -import Run from "@/components/runbooks/editor/blocks/Run"; -import Directory from "@/components/runbooks/editor/blocks/Directory"; - -import { DeleteBlock } from "@/components/runbooks/editor/ui/DeleteBlockButton"; -import { AtuinState, 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. -const schema = BlockNoteSchema.create({ - blockSpecs: { - // Adds all default blocks. - ...defaultBlockSpecs, - - // Adds the code block. - run: Run, - directory: Directory, - }, -}); - -// Slash menu item to insert an Alert block -const insertRun = (editor: typeof schema.BlockNoteEditor) => ({ - title: "Code", - onItemClick: () => { - insertOrUpdateBlock(editor, { - type: "run", - }); - }, - icon: , - aliases: ["code", "run"], - group: "Execute", -}); - -const insertDirectory = (editor: typeof schema.BlockNoteEditor) => ({ - title: "Directory", - onItemClick: () => { - insertOrUpdateBlock(editor, { - type: "directory", - }); - }, - icon: , - aliases: ["directory", "dir", "folder"], - group: "Execute", -}); - -export default function Editor() { - const runbookId = useStore((store: AtuinState) => store.currentRunbook); - const refreshRunbooks = useStore( - (store: AtuinState) => store.refreshRunbooks, - ); - let [runbook, setRunbook] = useState(null); - - useEffect(() => { - if (!runbookId) return; - - const fetchRunbook = async () => { - let rb = await Runbook.load(runbookId); - - setRunbook(rb); - }; - - fetchRunbook(); - }, [runbookId]); - - const onChange = async () => { - if (!runbook) return; - - console.log("saved!"); - runbook.name = fetchName(); - if (editor) runbook.content = JSON.stringify(editor.document); - - await runbook.save(); - refreshRunbooks(); - }; - - const debouncedOnChange = useDebounceCallback(onChange, 1000); - - 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 fetchName = (): string => { - // Infer the title from the first text block - if (!editor) return "Untitled"; - - let blocks = editor.document; - for (const block of blocks) { - if (block.type == "heading" || block.type == "paragraph") { - if (block.content.length == 0) continue; - // @ts-ignore - if (block.content[0].text.length == 0) continue; - - // @ts-ignore - return block.content[0].text; - } - } - - return "Untitled"; - }; - - if (!runbook) { - return ( -
- -
- ); - } - - if (editor === undefined) { - return ( -
- -
- ); - } - - // Renders the editor instance. - return ( -
- - - filterSuggestionItems( - [ - ...getDefaultReactSlashMenuItems(editor), - insertRun(editor), - insertDirectory(editor), - ], - query, - ) - } - /> - - ( - - - - - )} - /> - -
- ); -} diff --git a/ui/src/components/runbooks/editor/blocks/Directory/index.tsx b/ui/src/components/runbooks/editor/blocks/Directory/index.tsx deleted file mode 100644 index 3e4f93d9..00000000 --- a/ui/src/components/runbooks/editor/blocks/Directory/index.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { useState } from "react"; -import { Input, Tooltip, Button } from "@nextui-org/react"; -import { FolderInputIcon } from "lucide-react"; - -// @ts-ignore -import { createReactBlockSpec } from "@blocknote/react"; - -import { open } from "@tauri-apps/plugin-dialog"; - -interface DirectoryProps { - path: string; - onInputChange: (val: string) => void; -} - -const Directory = ({ path, onInputChange }: DirectoryProps) => { - const [value, setValue] = useState(path); - - const selectFolder = async () => { - const path = await open({ - multiple: false, - directory: true, - }); - - setValue(path || ""); - onInputChange(path || ""); - }; - - return ( -
- -
-
- -
- -
- { - setValue(val); - onInputChange(val); - }} - /> -
-
-
-
- ); -}; - -export default createReactBlockSpec( - { - type: "directory", - propSchema: { - path: { default: "" }, - }, - content: "none", - }, - { - // @ts-ignore - render: ({ block, editor, code, type }) => { - const onInputChange = (val: string) => { - editor.updateBlock(block, { - // @ts-ignore - props: { ...block.props, path: val }, - }); - }; - - return ( - - ); - }, - }, -); diff --git a/ui/src/components/runbooks/editor/blocks/Run/extensions.ts b/ui/src/components/runbooks/editor/blocks/Run/extensions.ts deleted file mode 100644 index 76fc4343..00000000 --- a/ui/src/components/runbooks/editor/blocks/Run/extensions.ts +++ /dev/null @@ -1,158 +0,0 @@ -// Based on the basicSetup extension, as suggested by the source. Customized for Atuin. - -import { - KeyBinding, - lineNumbers, - highlightActiveLineGutter, - highlightSpecialChars, - drawSelection, - dropCursor, - rectangularSelection, - crosshairCursor, - highlightActiveLine, - keymap, -} from "@codemirror/view"; -import { EditorState, Extension } from "@codemirror/state"; -import { history, defaultKeymap, historyKeymap } from "@codemirror/commands"; -import { highlightSelectionMatches, searchKeymap } from "@codemirror/search"; - -import { - closeBrackets, - autocompletion, - closeBracketsKeymap, - completionKeymap, - CompletionContext, -} from "@codemirror/autocomplete"; - -import { - foldGutter, - indentOnInput, - syntaxHighlighting, - defaultHighlightStyle, - bracketMatching, - indentUnit, - foldKeymap, -} from "@codemirror/language"; - -import { lintKeymap } from "@codemirror/lint"; -import { invoke } from "@tauri-apps/api/core"; - -export interface MinimalSetupOptions { - highlightSpecialChars?: boolean; - history?: boolean; - drawSelection?: boolean; - syntaxHighlighting?: boolean; - - defaultKeymap?: boolean; - historyKeymap?: boolean; -} - -export interface BasicSetupOptions extends MinimalSetupOptions { - lineNumbers?: boolean; - highlightActiveLineGutter?: boolean; - foldGutter?: boolean; - dropCursor?: boolean; - allowMultipleSelections?: boolean; - indentOnInput?: boolean; - bracketMatching?: boolean; - closeBrackets?: boolean; - autocompletion?: boolean; - rectangularSelection?: boolean; - crosshairCursor?: boolean; - highlightActiveLine?: boolean; - highlightSelectionMatches?: boolean; - - closeBracketsKeymap?: boolean; - searchKeymap?: boolean; - foldKeymap?: boolean; - completionKeymap?: boolean; - lintKeymap?: boolean; - tabSize?: number; -} - -function myCompletions(context: CompletionContext) { - let word = context.matchBefore(/^.*/); - - if (!word) return null; - if (word.from == word.to && !context.explicit) return null; - - return invoke("prefix_search", { query: word.text }).then( - // @ts-ignore - (results: string[]) => { - let options = results.map((i) => { - return { label: i, type: "text" }; - }); - - return { - from: word.from, - options, - }; - }, - ); -} - -const buildAutocomplete = (): Extension => { - let ac = autocompletion({ override: [myCompletions] }); - - return ac; -}; - -export const extensions = (options: BasicSetupOptions = {}): Extension[] => { - const { crosshairCursor: initCrosshairCursor = false } = options; - - let keymaps: KeyBinding[] = []; - if (options.closeBracketsKeymap !== false) { - keymaps = keymaps.concat(closeBracketsKeymap); - } - if (options.defaultKeymap !== false) { - keymaps = keymaps.concat(defaultKeymap); - } - if (options.searchKeymap !== false) { - keymaps = keymaps.concat(searchKeymap); - } - if (options.historyKeymap !== false) { - keymaps = keymaps.concat(historyKeymap); - } - if (options.foldKeymap !== false) { - keymaps = keymaps.concat(foldKeymap); - } - if (options.completionKeymap !== false) { - keymaps = keymaps.concat(completionKeymap); - } - if (options.lintKeymap !== false) { - keymaps = keymaps.concat(lintKeymap); - } - const extensions: Extension[] = []; - if (options.lineNumbers !== false) extensions.push(lineNumbers()); - if (options.highlightActiveLineGutter !== false) - extensions.push(highlightActiveLineGutter()); - if (options.highlightSpecialChars !== false) - extensions.push(highlightSpecialChars()); - if (options.history !== false) extensions.push(history()); - if (options.foldGutter !== false) extensions.push(foldGutter()); - if (options.drawSelection !== false) extensions.push(drawSelection()); - if (options.dropCursor !== false) extensions.push(dropCursor()); - if (options.allowMultipleSelections !== false) - extensions.push(EditorState.allowMultipleSelections.of(true)); - if (options.indentOnInput !== false) extensions.push(indentOnInput()); - if (options.syntaxHighlighting !== false) - extensions.push( - syntaxHighlighting(defaultHighlightStyle, { fallback: true }), - ); - - if (options.bracketMatching !== false) extensions.push(bracketMatching()); - if (options.closeBrackets !== false) extensions.push(closeBrackets()); - if (options.autocompletion !== false) extensions.push(buildAutocomplete()); - - if (options.rectangularSelection !== false) - extensions.push(rectangularSelection()); - if (initCrosshairCursor !== false) extensions.push(crosshairCursor()); - if (options.highlightActiveLine !== false) - extensions.push(highlightActiveLine()); - if (options.highlightSelectionMatches !== false) - extensions.push(highlightSelectionMatches()); - if (options.tabSize && typeof options.tabSize === "number") - extensions.push(indentUnit.of(" ".repeat(options.tabSize))); - - return extensions.concat([keymap.of(keymaps.flat())]).filter(Boolean); -}; diff --git a/ui/src/components/runbooks/editor/blocks/Run/index.css b/ui/src/components/runbooks/editor/blocks/Run/index.css deleted file mode 100644 index e854c03b..00000000 --- a/ui/src/components/runbooks/editor/blocks/Run/index.css +++ /dev/null @@ -1,9 +0,0 @@ -ProseMirror-focused { - outline: none !important; - box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1) !important; -} - -.cm-editor.cm-focused { - outline: none !important; - box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1) !important; -} diff --git a/ui/src/components/runbooks/editor/blocks/Run/index.tsx b/ui/src/components/runbooks/editor/blocks/Run/index.tsx deleted file mode 100644 index bef083ba..00000000 --- a/ui/src/components/runbooks/editor/blocks/Run/index.tsx +++ /dev/null @@ -1,229 +0,0 @@ -// @ts-ignore -import { createReactBlockSpec } from "@blocknote/react"; - -import "./index.css"; - -import CodeMirror from "@uiw/react-codemirror"; -import { keymap } from "@codemirror/view"; -import { langs } from "@uiw/codemirror-extensions-langs"; - -import { Play, Square } from "lucide-react"; -import { useState } from "react"; - -import { extensions } from "./extensions"; -import { platform } from "@tauri-apps/plugin-os"; -import { invoke } from "@tauri-apps/api/core"; -import Terminal from "./terminal.tsx"; - -import "@xterm/xterm/css/xterm.css"; -import { AtuinState, useStore } from "@/state/store.ts"; - -interface RunBlockProps { - onChange: (val: string) => void; - onRun?: (pty: string) => void; - onStop?: (pty: string) => void; - id: string; - code: string; - type: string; - pty: string; - isEditable: boolean; - editor: any; -} - -const findFirstParentOfType = (editor: any, id: string, type: string): any => { - // TODO: the types for blocknote aren't working. Now I'm doing this sort of shit, - // really need to fix that. - const document = editor.document; - var lastOfType = null; - - // Iterate through ALL of the blocks. - for (let i = 0; i < document.length; i++) { - if (document[i].id == id) return lastOfType; - - if (document[i].type == type) lastOfType = document[i]; - } - - return lastOfType; -}; - -const RunBlock = ({ - onChange, - id, - code, - isEditable, - onRun, - onStop, - pty, - editor, -}: RunBlockProps) => { - const [value, setValue] = useState(code); - const cleanupPtyTerm = useStore((store: AtuinState) => store.cleanupPtyTerm); - const terminals = useStore((store: AtuinState) => store.terminals); - - const [currentRunbook, incRunbookPty, decRunbookPty] = useStore( - (store: AtuinState) => [ - store.currentRunbook, - store.incRunbookPty, - store.decRunbookPty, - ], - ); - - const isRunning = pty !== null && pty !== ""; - - const handleToggle = async (event: any | null) => { - if (event) event.stopPropagation(); - - // If there's no code, don't do anything - if (!value) return; - - if (isRunning) { - await invoke("pty_kill", { pid: pty }); - - terminals[pty].terminal.dispose(); - cleanupPtyTerm(pty); - - if (onStop) onStop(pty); - if (currentRunbook) decRunbookPty(currentRunbook); - } - - if (!isRunning) { - let cwd = findFirstParentOfType(editor, id, "directory"); - - if (cwd) { - cwd = cwd.props.path; - } else { - cwd = "~"; - } - - let pty = await invoke("pty_open", { cwd }); - if (onRun) onRun(pty); - - if (currentRunbook) incRunbookPty(currentRunbook); - - let isWindows = platform() == "windows"; - let cmdEnd = isWindows ? "\r\n" : "\n"; - - let val = !value.endsWith("\n") ? value + cmdEnd : value; - await invoke("pty_write", { pid: pty, data: val }); - } - }; - - const handleCmdEnter = () => { - handleToggle(null); - return true; - }; - - const customKeymap = keymap.of([ - { - key: "Mod-Enter", - run: handleCmdEnter, - }, - ]); - - return ( -
-
-
- -
-
- { - setValue(val); - onChange(val); - }} - extensions={[customKeymap, ...extensions(), langs.shell()]} - basicSetup={false} - /> -
- {pty && } -
-
-
-
- ); -}; - -export default createReactBlockSpec( - { - type: "run", - propSchema: { - type: { - default: "bash", - }, - code: { default: "" }, - pty: { default: "" }, - global: { default: false }, - }, - content: "none", - }, - { - // @ts-ignore - render: ({ block, editor, code, type }) => { - const onInputChange = (val: string) => { - editor.updateBlock(block, { - // @ts-ignore - props: { ...block.props, code: val }, - }); - }; - - const onRun = (pty: string) => { - editor.updateBlock(block, { - // @ts-ignore - props: { ...block.props, pty: pty }, - }); - }; - - const onStop = (_pty: string) => { - editor?.updateBlock(block, { - props: { ...block.props, pty: "" }, - }); - }; - - return ( - - ); - }, - toExternalHTML: ({ block }) => { - return ( -
-          {block?.props?.code}
-        
- ); - }, - }, -); diff --git a/ui/src/components/runbooks/editor/blocks/Run/terminal.tsx b/ui/src/components/runbooks/editor/blocks/Run/terminal.tsx deleted file mode 100644 index a6dc589f..00000000 --- a/ui/src/components/runbooks/editor/blocks/Run/terminal.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { useState, useEffect, useRef } from "react"; -import { listen } from "@tauri-apps/api/event"; -import "@xterm/xterm/css/xterm.css"; -import { useStore } from "@/state/store"; -import { invoke } from "@tauri-apps/api/core"; -import { IDisposable } from "@xterm/xterm"; - -const usePersistentTerminal = (pty: string) => { - const newPtyTerm = useStore((store) => store.newPtyTerm); - const terminals = useStore((store) => store.terminals); - const [isReady, setIsReady] = useState(false); - - useEffect(() => { - if (!terminals.hasOwnProperty(pty)) { - // create a new terminal and store it in the store. - // this means we can resume the same instance even across mount/dismount - newPtyTerm(pty); - } - - setIsReady(true); - - return () => { - // We don't dispose of the terminal when the component unmounts - }; - }, [pty, terminals, newPtyTerm]); - - return { terminalData: terminals[pty], isReady }; -}; - -const TerminalComponent = ({ pty }: any) => { - const terminalRef = useRef(null); - const { terminalData, isReady } = usePersistentTerminal(pty); - const [isAttached, setIsAttached] = useState(false); - const cleanupListenerRef = useRef<(() => void) | null>(null); - const keyDispose = useRef(null); - - useEffect(() => { - // no pty? no terminal - if (pty == null) return; - - // the terminal may still be being created so hold off - if (!isReady) return; - - const windowResize = () => { - if (!terminalData || !terminalData.fitAddon) return; - - terminalData.fitAddon.fit(); - }; - - // terminal object needs attaching to a ref to a div - if (!isAttached && terminalData && terminalData.terminal) { - // If it's never been attached, attach it - if (!terminalData.terminal.element && terminalRef.current) { - terminalData.terminal.open(terminalRef.current); - - // it might have been previously attached, but need moving elsewhere - } else if (terminalData && terminalRef.current) { - // @ts-ignore - terminalRef.current.appendChild(terminalData.terminal.element); - } - - terminalData.fitAddon.fit(); - setIsAttached(true); - - window.addEventListener("resize", windowResize); - - const disposeOnKey = terminalData.terminal.onKey(async (event) => { - await invoke("pty_write", { pid: pty, data: event.key }); - }); - - keyDispose.current = disposeOnKey; - } - - listen(`pty-${pty}`, (event: any) => { - terminalData.terminal.write(event.payload); - }).then((ul) => { - cleanupListenerRef.current = ul; - }); - - // Customize further as needed - return () => { - if ( - terminalData && - terminalData.terminal && - terminalData.terminal.element - ) { - // Instead of removing, we just detach - if (terminalData.terminal.element.parentElement) { - terminalData.terminal.element.parentElement.removeChild( - terminalData.terminal.element, - ); - } - setIsAttached(false); - } - - if (cleanupListenerRef.current) { - cleanupListenerRef.current(); - } - - if (keyDispose.current) keyDispose.current.dispose(); - - window.removeEventListener("resize", windowResize); - }; - }, [terminalData, isReady]); - - if (!isReady) return null; - - return ( -
- ); -}; - -export default TerminalComponent; diff --git a/ui/src/components/runbooks/editor/index.css b/ui/src/components/runbooks/editor/index.css deleted file mode 100644 index 067cc500..00000000 --- a/ui/src/components/runbooks/editor/index.css +++ /dev/null @@ -1,7 +0,0 @@ -.editor a { - color: #0000ee; -} - -.editor a:hover { - cursor: pointer; -} diff --git a/ui/src/components/runbooks/editor/ui/DeleteBlockButton.tsx b/ui/src/components/runbooks/editor/ui/DeleteBlockButton.tsx deleted file mode 100644 index 84a9f5c8..00000000 --- a/ui/src/components/runbooks/editor/ui/DeleteBlockButton.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { - SideMenuProps, - useBlockNoteEditor, - useComponentsContext, -} from "@blocknote/react"; -import { TrashIcon } from "lucide-react"; - -// Custom Side Menu button to remove the hovered block. -export function DeleteBlock(props: SideMenuProps) { - const editor = useBlockNoteEditor(); - - const Components = useComponentsContext()!; - - return ( - { - editor.removeBlocks([props.block]); - }} - /> - } - /> - ); -} -- cgit v1.3.1