mirror of
https://github.com/atuinsh/atuin.git
synced 2024-11-25 09:44:03 +01:00
feat(gui): add runbook list, ability to create and delete, sql storage (#2282)
* wip * saving works :)) * functioning delete button * persist selection properly
This commit is contained in:
parent
947305fc7f
commit
7eb985b616
90
ui/backend/Cargo.lock
generated
90
ui/backend/Cargo.lock
generated
@ -140,6 +140,15 @@ version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
version = "0.5.3"
|
||||
@ -349,6 +358,7 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"clap",
|
||||
"config",
|
||||
"crossterm",
|
||||
"crypto_secretbox",
|
||||
"directories",
|
||||
"eyre",
|
||||
@ -360,9 +370,11 @@ dependencies = [
|
||||
"indicatif",
|
||||
"interim",
|
||||
"itertools",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"memchr",
|
||||
"minspan",
|
||||
"palette",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"reqwest 0.11.27",
|
||||
@ -378,6 +390,8 @@ dependencies = [
|
||||
"shellexpand",
|
||||
"sql-builder",
|
||||
"sqlx",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tiny-bip39",
|
||||
@ -609,6 +623,12 @@ version = "3.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||
|
||||
[[package]]
|
||||
name = "by_address"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.16.0"
|
||||
@ -1686,6 +1706,12 @@ dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fast-srgb8"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.1.0"
|
||||
@ -1746,7 +1772,7 @@ checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"spin 0.9.8",
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2950,11 +2976,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
dependencies = [
|
||||
"spin 0.5.2",
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3686,6 +3712,31 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "palette"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"fast-srgb8",
|
||||
"palette_derive",
|
||||
"phf 0.11.2",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "palette_derive"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30"
|
||||
dependencies = [
|
||||
"by_address",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.18.3"
|
||||
@ -4473,7 +4524,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom 0.2.15",
|
||||
"libc",
|
||||
"spin 0.9.8",
|
||||
"spin",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
@ -5200,12 +5251,6 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
@ -5521,6 +5566,28 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.5.0"
|
||||
@ -6484,6 +6551,7 @@ dependencies = [
|
||||
"cocoa",
|
||||
"comrak",
|
||||
"eyre",
|
||||
"lazy_static",
|
||||
"nix 0.29.0",
|
||||
"portable-pty",
|
||||
"serde",
|
||||
|
@ -18,7 +18,7 @@ atuin-dotfiles = { path = "../../crates/atuin-dotfiles", version = "0.4.0" }
|
||||
atuin-history = { path = "../../crates/atuin-history", version = "0.3.0" }
|
||||
|
||||
eyre = "0.6"
|
||||
tauri = { version = "2.0.0-beta", features = [ "tray-icon"] }
|
||||
tauri = { version = "2.0.0-beta", features = ["tray-icon"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
time = "0.3.36"
|
||||
@ -33,6 +33,7 @@ portable-pty = "0.8.1"
|
||||
vt100 = "0.15.2"
|
||||
bytes = "1.6.0"
|
||||
nix = "0.29.0"
|
||||
lazy_static = "1.5.0"
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
cocoa = "0.25"
|
||||
|
@ -284,12 +284,16 @@ fn main() {
|
||||
dotfiles::vars::delete_var,
|
||||
dotfiles::vars::set_var,
|
||||
])
|
||||
.plugin(tauri_plugin_sql::Builder::default().build())
|
||||
.plugin(tauri_plugin_sql::Builder::default().add_migrations("sqlite:runbooks.db", run::migrations::migrations()).build())
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(tauri_plugin_single_instance::init(|app, args, cwd| {
|
||||
let _ = show_window(app);
|
||||
|
||||
}))
|
||||
.manage(state::AtuinState::default())
|
||||
.setup(|app|{
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
13
ui/backend/src/run/migrations.rs
Normal file
13
ui/backend/src/run/migrations.rs
Normal file
@ -0,0 +1,13 @@
|
||||
use lazy_static::lazy_static;
|
||||
use tauri_plugin_sql::{Builder, Migration, MigrationKind};
|
||||
|
||||
pub fn migrations() -> Vec<Migration> {
|
||||
vec![
|
||||
Migration {
|
||||
version: 1,
|
||||
description: "create_initial_tables",
|
||||
sql: "CREATE TABLE runbooks(id string PRIMARY KEY, name TEXT, content TEXT, created bigint, updated bigint);",
|
||||
kind: MigrationKind::Up,
|
||||
}
|
||||
]
|
||||
}
|
@ -1 +1,2 @@
|
||||
pub mod migrations;
|
||||
pub mod pty;
|
||||
|
@ -14,8 +14,6 @@
|
||||
"title": "Atuin",
|
||||
"width": 1200,
|
||||
"height": 800,
|
||||
"minWidth": 1000,
|
||||
"minHeight": 500,
|
||||
"titleBarStyle": "Overlay",
|
||||
"hiddenTitle": true
|
||||
}
|
||||
|
@ -10,9 +10,9 @@
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blocknote/core": "^0.14.5",
|
||||
"@blocknote/mantine": "^0.14.2",
|
||||
"@blocknote/react": "^0.14.6",
|
||||
"@blocknote/core": "^0.15.0",
|
||||
"@blocknote/mantine": "^0.15.0",
|
||||
"@blocknote/react": "^0.15.0",
|
||||
"@codemirror/autocomplete": "^6.16.3",
|
||||
"@codemirror/commands": "^6.6.0",
|
||||
"@codemirror/language": "^6.10.2",
|
||||
@ -52,8 +52,8 @@
|
||||
"react": "^18.3.1",
|
||||
"react-activity-calendar": "^2.2.10",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-spinners": "^0.14.1",
|
||||
"react-router-dom": "^6.24.1",
|
||||
"react-spinners": "^0.14.1",
|
||||
"react-tooltip": "^5.27.0",
|
||||
"react-window": "^1.8.10",
|
||||
"react-window-infinite-loader": "^1.0.9",
|
||||
@ -61,6 +61,7 @@
|
||||
"tailwind-merge": "^2.4.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"uuidv7": "^1.0.1",
|
||||
"vaul": "^0.9.1",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
|
9472
ui/pnpm-lock.yaml
9472
ui/pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -131,7 +131,7 @@ function App() {
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh w-full select-none">
|
||||
<div className="relative flex h-full flex-col !border-r-small border-divider p-6 transition-width px-2 pb-6 pt-9 w-16 items-center">
|
||||
<div className="relative flex h-full flex-col !border-r-small border-divider transition-width pb-6 pt-9 min-w-[4.5rem] items-center">
|
||||
<div className="flex items-center gap-0 px-3 justify-center">
|
||||
<div className="flex h-8 w-8">
|
||||
<img src={icon} alt="icon" className="h-8 w-8" />
|
||||
|
120
ui/src/components/runbooks/List.tsx
Normal file
120
ui/src/components/runbooks/List.tsx
Normal file
@ -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;
|
@ -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) =>
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
@ -7,8 +7,10 @@ import "./styles.css";
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<NextUIProvider>
|
||||
<div data-tauri-drag-region className="w-full h-8 absolute" />
|
||||
<App />
|
||||
<main className="text-foreground bg-background">
|
||||
<div data-tauri-drag-region className="w-full h-8 absolute" />
|
||||
<App />
|
||||
</main>
|
||||
</NextUIProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
@ -1,9 +1,20 @@
|
||||
import Editor from "@/components/runbooks/editor/Editor";
|
||||
import List from "@/components/runbooks/List";
|
||||
import { useStore } from "@/state/store";
|
||||
|
||||
export default function Runbooks() {
|
||||
const currentRunbook = useStore((store) => store.currentRunbook);
|
||||
|
||||
return (
|
||||
<div className="w-full flex-1 flex-col p-4">
|
||||
<Editor />
|
||||
<div className="w-full flex flex-row ">
|
||||
<List />
|
||||
{currentRunbook && <Editor />}
|
||||
|
||||
{!currentRunbook && (
|
||||
<div className="flex align-middle justify-center flex-col h-screen w-full">
|
||||
<h1 className="text-center">Select or create a runbook</h1>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
124
ui/src/state/runbooks/runbook.ts
Normal file
124
ui/src/state/runbooks/runbook.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import Database from "@tauri-apps/plugin-sql";
|
||||
import { uuidv7 } from "uuidv7";
|
||||
|
||||
export default class Runbook {
|
||||
id: string;
|
||||
created: Date;
|
||||
updated: Date;
|
||||
|
||||
private _name: string;
|
||||
private _content: string;
|
||||
|
||||
set name(value: string) {
|
||||
this.updated = new Date();
|
||||
this._name = value;
|
||||
}
|
||||
|
||||
set content(value: string) {
|
||||
this.updated = new Date();
|
||||
this._content = value;
|
||||
}
|
||||
|
||||
get content() {
|
||||
return this._content;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
name: string,
|
||||
content: string,
|
||||
created: Date,
|
||||
updated: Date,
|
||||
) {
|
||||
this.id = id;
|
||||
this._name = name;
|
||||
this._content = content;
|
||||
this.created = created;
|
||||
this.updated = updated;
|
||||
}
|
||||
|
||||
/// Create a new Runbook, and automatically generate an ID.
|
||||
public static async create(): Promise<Runbook> {
|
||||
let now = new Date();
|
||||
|
||||
// Initialize with the same value for created/updated, to avoid needing null.
|
||||
let runbook = new Runbook(uuidv7(), "", "", now, now);
|
||||
await runbook.save();
|
||||
|
||||
return runbook;
|
||||
}
|
||||
|
||||
public static async load(id: String): Promise<Runbook | null> {
|
||||
const db = await Database.load("sqlite:runbooks.db");
|
||||
|
||||
let res = await db.select<any[]>("select * from runbooks where id = $1", [
|
||||
id,
|
||||
]);
|
||||
|
||||
if (res.length == 0) return null;
|
||||
|
||||
let rb = res[0];
|
||||
|
||||
return new Runbook(
|
||||
rb.id,
|
||||
rb.name,
|
||||
rb.content,
|
||||
new Date(rb.created / 1000000),
|
||||
new Date(rb.updated / 1000000),
|
||||
);
|
||||
}
|
||||
|
||||
static async all(): Promise<Runbook[]> {
|
||||
const db = await Database.load("sqlite:runbooks.db");
|
||||
|
||||
let res = await db.select<any[]>(
|
||||
"select * from runbooks order by updated desc",
|
||||
);
|
||||
|
||||
return res.map((i) => {
|
||||
return new Runbook(
|
||||
i.id,
|
||||
i.name,
|
||||
i.content,
|
||||
new Date(i.created / 1000000),
|
||||
new Date(i.updated / 1000000),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async save() {
|
||||
const db = await Database.load("sqlite:runbooks.db");
|
||||
|
||||
await db.execute(
|
||||
`insert into runbooks(id, name, content, created, updated)
|
||||
values ($1, $2, $3, $4, $5)
|
||||
|
||||
on conflict(id) do update
|
||||
set
|
||||
name=$2,
|
||||
content=$3,
|
||||
updated=$5`,
|
||||
|
||||
// getTime returns a timestamp as unix milliseconds
|
||||
// we won't need or use the resolution here, but elsewhere Atuin stores timestamps in sqlite as nanoseconds since epoch
|
||||
// let's do that across the board to avoid mistakes
|
||||
[
|
||||
this.id,
|
||||
this._name,
|
||||
this._content,
|
||||
this.created.getTime() * 1000000,
|
||||
this.updated.getTime() * 1000000,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public static async delete(id: string) {
|
||||
const db = await Database.load("sqlite:runbooks.db");
|
||||
|
||||
await db.execute("delete from runbooks where id=$1", [id]);
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ import {
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { sessionToken, settings } from "./client";
|
||||
import { getWeekInfo } from "@/lib/utils";
|
||||
import Runbook from "./runbooks/runbook";
|
||||
|
||||
// I'll probs want to slice this up at some point, but for now a
|
||||
// big blobby lump of state is fine.
|
||||
@ -30,14 +31,19 @@ interface AtuinState {
|
||||
shellHistory: ShellHistory[];
|
||||
calendar: any[];
|
||||
weekStart: number;
|
||||
runbooks: Runbook[];
|
||||
currentRunbook: String | null;
|
||||
|
||||
refreshHomeInfo: () => void;
|
||||
refreshCalendar: () => void;
|
||||
refreshAliases: () => void;
|
||||
refreshVars: () => void;
|
||||
refreshUser: () => void;
|
||||
refreshRunbooks: () => void;
|
||||
refreshShellHistory: (query?: string) => void;
|
||||
historyNextPage: (query?: string) => void;
|
||||
|
||||
setCurrentRunbook: (id: String) => void;
|
||||
}
|
||||
|
||||
let state = (set: any, get: any): AtuinState => ({
|
||||
@ -47,6 +53,8 @@ let state = (set: any, get: any): AtuinState => ({
|
||||
vars: [],
|
||||
shellHistory: [],
|
||||
calendar: [],
|
||||
runbooks: [],
|
||||
currentRunbook: "",
|
||||
|
||||
weekStart: getWeekInfo().firstDay,
|
||||
|
||||
@ -68,6 +76,11 @@ let state = (set: any, get: any): AtuinState => ({
|
||||
});
|
||||
},
|
||||
|
||||
refreshRunbooks: async () => {
|
||||
let runbooks = await Runbook.all();
|
||||
set({ runbooks });
|
||||
},
|
||||
|
||||
refreshShellHistory: (query?: string) => {
|
||||
if (query) {
|
||||
invoke("search", { query: query })
|
||||
@ -141,6 +154,10 @@ let state = (set: any, get: any): AtuinState => ({
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setCurrentRunbook: (id: String) => {
|
||||
set({ currentRunbook: id });
|
||||
},
|
||||
});
|
||||
|
||||
export const useStore = create<AtuinState>()(
|
||||
|
Loading…
Reference in New Issue
Block a user