From f8c963c7d668fc57680f25413f20bc207d4bf64a Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Tue, 23 Jul 2024 13:18:54 +0100 Subject: feat(gui): clean up home page, fix a few bugs (#2304) * wip home screen changes * more * adjust * fixes and things * patch runbook pty check --- ui/src/components/history/HistoryRow.tsx | 35 +- ui/src/components/home/QuickActions.tsx | 1 + ui/src/components/runbooks/List.tsx | 15 +- ui/src/components/runbooks/editor/Editor.tsx | 74 +++-- .../runbooks/editor/blocks/RunBlock/index.tsx | 20 +- ui/src/components/ui/card.tsx | 79 +++++ ui/src/components/ui/chart.tsx | 363 +++++++++++++++++++++ 7 files changed, 527 insertions(+), 60 deletions(-) create mode 100644 ui/src/components/home/QuickActions.tsx create mode 100644 ui/src/components/ui/card.tsx create mode 100644 ui/src/components/ui/chart.tsx (limited to 'ui/src/components') diff --git a/ui/src/components/history/HistoryRow.tsx b/ui/src/components/history/HistoryRow.tsx index 98d271fb..4d893e61 100644 --- a/ui/src/components/history/HistoryRow.tsx +++ b/ui/src/components/history/HistoryRow.tsx @@ -11,6 +11,7 @@ import "prismjs/components/prism-bash"; import Drawer from "../Drawer"; import HistoryInspect from "./HistoryInspect"; +import { cn } from "@/lib/utils"; function msToTime(ms: number) { let milliseconds = parseInt(ms.toFixed(1)); @@ -26,25 +27,31 @@ function msToTime(ms: number) { else return days + " Days"; } -export default function HistoryRow({ h }: any) { +export default function HistoryRow({ h, compact }: any) { return (
  • -
    -

    - {DateTime.fromMillis(h.timestamp / 1000000).toLocaleString( - DateTime.TIME_WITH_SECONDS, - )} -

    -

    - {DateTime.fromMillis(h.timestamp / 1000000).toLocaleString( - DateTime.DATE_SHORT, - )} -

    -
    + {!compact && ( +
    +

    + {DateTime.fromMillis(h.timestamp / 1000000).toLocaleString( + DateTime.TIME_WITH_SECONDS, + )} +

    +

    + {DateTime.fromMillis(h.timestamp / 1000000).toLocaleString( + DateTime.DATE_SHORT, + )} +

    +
    + )}
    {
    { + items={runbooks.map((runbook: any): any => { return [runbook, runbookInfo[runbook.id]]; })} variant="flat" aria-label="Runbook list" selectionMode="single" - selectedKeys={[currentRunbook]} + selectedKeys={currentRunbook ? [currentRunbook] : []} itemClasses={{ base: "data-[selected=true]:bg-gray-200" }} topContent={ @@ -74,7 +69,7 @@ const NoteSidebar = () => { } > - {([runbook, info]) => ( + {([runbook, info]: [Runbook, { ptys: number }]) => ( { @@ -124,7 +119,7 @@ const NoteSidebar = () => {
    {DateTime.fromJSDate(runbook.updated).toLocaleString( - DateTime.DATETIME_SIMPLE, + DateTime.DATETIME_SHORT, )}
    diff --git a/ui/src/components/runbooks/editor/Editor.tsx b/ui/src/components/runbooks/editor/Editor.tsx index 98a6a282..bbf594d8 100644 --- a/ui/src/components/runbooks/editor/Editor.tsx +++ b/ui/src/components/runbooks/editor/Editor.tsx @@ -1,37 +1,47 @@ import { useEffect, useMemo, useState } from "react"; -import "@blocknote/core/fonts/inter.css"; -import "@blocknote/mantine/style.css"; 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 "@blocknote/core/fonts/inter.css"; - import { + //@ts-ignore SuggestionMenuController, + // @ts-ignore AddBlockButton, + // @ts-ignore getDefaultReactSlashMenuItems, - useCreateBlockNote, + // @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 { 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 { AtuinState, useStore } from "@/state/store"; import Runbook from "@/state/runbooks/runbook"; // Our schema with block specs, which contain the configs and implementations for blocks @@ -60,8 +70,10 @@ const insertRun = (editor: typeof schema.BlockNoteEditor) => ({ }); export default function Editor() { - const runbookId = useStore((store) => store.currentRunbook); - const refreshRunbooks = useStore((store) => store.refreshRunbooks); + const runbookId = useStore((store: AtuinState) => store.currentRunbook); + const refreshRunbooks = useStore( + (store: AtuinState) => store.refreshRunbooks, + ); let [runbook, setRunbook] = useState(null); useEffect(() => { @@ -76,43 +88,43 @@ export default function Editor() { 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); + if (editor) runbook.content = JSON.stringify(editor.document); await runbook.save(); - await refreshRunbooks(); + 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; } } @@ -120,6 +132,14 @@ export default function Editor() { return "Untitled"; }; + if (!runbook) { + return ( +
    + +
    + ); + } + if (editor === undefined) { return (
    @@ -139,7 +159,7 @@ export default function Editor() { > + getItems={async (query: any) => filterSuggestionItems( [...getDefaultReactSlashMenuItems(editor), insertRun(editor)], query, @@ -148,7 +168,7 @@ export default function Editor() { /> ( + sideMenu={(props: any) => ( diff --git a/ui/src/components/runbooks/editor/blocks/RunBlock/index.tsx b/ui/src/components/runbooks/editor/blocks/RunBlock/index.tsx index 15653611..b3a96166 100644 --- a/ui/src/components/runbooks/editor/blocks/RunBlock/index.tsx +++ b/ui/src/components/runbooks/editor/blocks/RunBlock/index.tsx @@ -1,4 +1,4 @@ -import React from "react"; +// @ts-ignore import { createReactBlockSpec } from "@blocknote/react"; import "./index.css"; @@ -48,7 +48,7 @@ const RunBlock = ({ ], ); - const isRunning = pty !== null; + const isRunning = pty !== null && pty !== ""; const handleToggle = async (event: any | null) => { if (event) event.stopPropagation(); @@ -63,21 +63,21 @@ const RunBlock = ({ cleanupPtyTerm(pty); if (onStop) onStop(pty); - decRunbookPty(currentRunbook); + if (currentRunbook) decRunbookPty(currentRunbook); } if (!isRunning) { let pty = await invoke("pty_open"); if (onRun) onRun(pty); - incRunbookPty(currentRunbook); + if (currentRunbook) incRunbookPty(currentRunbook); let val = !value.endsWith("\n") ? value + "\r\n" : value; await invoke("pty_write", { pid: pty, data: val }); } }; - const handleCmdEnter = (view) => { + const handleCmdEnter = () => { handleToggle(null); return true; }; @@ -145,7 +145,7 @@ export default createReactBlockSpec( default: "bash", }, code: { default: "" }, - pty: { default: null }, + pty: { default: "" }, }, content: "none", }, @@ -154,19 +154,21 @@ export default createReactBlockSpec( 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: null }, + const onStop = (_pty: string) => { + editor?.updateBlock(block, { + props: { ...block.props, pty: "" }, }); }; diff --git a/ui/src/components/ui/card.tsx b/ui/src/components/ui/card.tsx new file mode 100644 index 00000000..afa13ecf --- /dev/null +++ b/ui/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

    +)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

    +)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

    +)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/ui/src/components/ui/chart.tsx b/ui/src/components/ui/chart.tsx new file mode 100644 index 00000000..a21d77ee --- /dev/null +++ b/ui/src/components/ui/chart.tsx @@ -0,0 +1,363 @@ +import * as React from "react" +import * as RechartsPrimitive from "recharts" + +import { cn } from "@/lib/utils" + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const + +export type ChartConfig = { + [k in string]: { + label?: React.ReactNode + icon?: React.ComponentType + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ) +} + +type ChartContextProps = { + config: ChartConfig +} + +const ChartContext = React.createContext(null) + +function useChart() { + const context = React.useContext(ChartContext) + + if (!context) { + throw new Error("useChart must be used within a ") + } + + return context +} + +const ChartContainer = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + config: ChartConfig + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"] + } +>(({ id, className, children, config, ...props }, ref) => { + const uniqueId = React.useId() + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` + + return ( + +
    + + + {children} + +
    +
    + ) +}) +ChartContainer.displayName = "Chart" + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([_, config]) => config.theme || config.color + ) + + if (!colorConfig.length) { + return null + } + + return ( +