mirror of
https://github.com/atuinsh/atuin.git
synced 2024-11-24 17:23:50 +01:00
feat(gui): clean up home page, fix a few bugs (#2304)
* wip home screen changes * more * adjust * fixes and things * patch runbook pty check
This commit is contained in:
parent
17ed668aac
commit
f8c963c7d6
4
ui/backend/Cargo.lock
generated
4
ui/backend/Cargo.lock
generated
@ -2866,9 +2866,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.1"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
@ -36,7 +36,7 @@ pub struct GlobalStats {
|
||||
pub last_30d: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct UIHistory {
|
||||
pub id: String,
|
||||
/// When the command was run.
|
||||
|
@ -30,6 +30,8 @@ struct HomeInfo {
|
||||
pub history_count: u64,
|
||||
pub username: Option<String>,
|
||||
pub last_sync: Option<String>,
|
||||
pub top_commands: Vec<(String, u64)>,
|
||||
pub recent_commands: Vec<UIHistory>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@ -138,6 +140,8 @@ async fn home_info() -> Result<HomeInfo, String> {
|
||||
let sqlite_store = SqliteStore::new(record_store_path, settings.local_timeout)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let db_path = PathBuf::from(settings.db_path.as_str());
|
||||
let db = HistoryDB::new(db_path, settings.local_timeout).await?;
|
||||
|
||||
let last_sync = Settings::last_sync()
|
||||
.map_err(|e| e.to_string())?
|
||||
@ -150,12 +154,20 @@ async fn home_info() -> Result<HomeInfo, String> {
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
|
||||
let history = db.list(None, None).await?;
|
||||
let stats = stats::compute(&settings, &history, 10, 1).map_or(vec![], |stats|stats.top[0..5].to_vec()).iter().map(|(commands, count)| (commands.join(" "), *count as u64)).collect();
|
||||
let recent = if history.len() > 5 {history[0..5].to_vec()} else {vec![]};
|
||||
let recent = recent.into_iter().map(|h|h.into()).collect();
|
||||
|
||||
let info = if !settings.logged_in() {
|
||||
HomeInfo {
|
||||
username: None,
|
||||
last_sync: None,
|
||||
record_count,
|
||||
history_count,
|
||||
top_commands: stats,
|
||||
recent_commands: recent,
|
||||
}
|
||||
} else {
|
||||
let client = atuin_client::api_client::Client::new(
|
||||
@ -176,6 +188,8 @@ async fn home_info() -> Result<HomeInfo, String> {
|
||||
last_sync: Some(last_sync.to_string()),
|
||||
record_count,
|
||||
history_count,
|
||||
top_commands: stats,
|
||||
recent_commands: recent,
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -10,30 +10,31 @@
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blocknote/core": "^0.15.0",
|
||||
"@blocknote/core": "^0.15.3",
|
||||
"@blocknote/mantine": "^0.15.3",
|
||||
"@blocknote/react": "^0.15.0",
|
||||
"@codemirror/autocomplete": "^6.16.3",
|
||||
"@blocknote/react": "^0.15.3",
|
||||
"@codemirror/autocomplete": "^6.17.0",
|
||||
"@codemirror/commands": "^6.6.0",
|
||||
"@codemirror/language": "^6.10.2",
|
||||
"@codemirror/lint": "^6.8.1",
|
||||
"@codemirror/search": "^6.5.6",
|
||||
"@codemirror/state": "^6.4.1",
|
||||
"@codemirror/view": "^6.28.4",
|
||||
"@headlessui/react": "^2.1.1",
|
||||
"@heroicons/react": "^2.1.4",
|
||||
"@nextui-org/react": "^2.4.2",
|
||||
"@codemirror/view": "^6.28.6",
|
||||
"@headlessui/react": "^2.1.2",
|
||||
"@heroicons/react": "^2.1.5",
|
||||
"@nextui-org/react": "^2.4.6",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.1",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tanstack/react-table": "^8.19.3",
|
||||
"@tanstack/react-virtual": "^3.8.1",
|
||||
"@tanstack/react-virtual": "^3.8.3",
|
||||
"@tauri-apps/api": "2.0.0-beta.15",
|
||||
"@tauri-apps/plugin-http": "2.0.0-beta.8",
|
||||
"@tauri-apps/plugin-shell": "2.0.0-beta.8",
|
||||
"@tauri-apps/plugin-sql": "2.0.0-beta.5",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@uiw/codemirror-extensions-langs": "^4.23.0",
|
||||
"@uiw/react-codemirror": "^4.23.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
@ -43,18 +44,18 @@
|
||||
"clsx": "^2.1.1",
|
||||
"core": "link:@tauri-apps/api/core",
|
||||
"date-fns": "^3.6.0",
|
||||
"framer-motion": "^11.2.13",
|
||||
"framer-motion": "^11.3.8",
|
||||
"highlight.js": "^11.10.0",
|
||||
"lucide-react": "^0.402.0",
|
||||
"luxon": "^3.4.4",
|
||||
"prism-react-renderer": "^2.3.1",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^18.3.1",
|
||||
"react-activity-calendar": "^2.2.10",
|
||||
"react-activity-calendar": "^2.2.11",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.24.1",
|
||||
"react-router-dom": "^6.25.1",
|
||||
"react-spinners": "^0.14.1",
|
||||
"react-tooltip": "^5.27.0",
|
||||
"react-tooltip": "^5.27.1",
|
||||
"react-window": "^1.8.10",
|
||||
"react-window-infinite-loader": "^1.0.9",
|
||||
"recharts": "^2.12.7",
|
||||
@ -72,10 +73,10 @@
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss": "^8.4.39",
|
||||
"tailwindcss": "^3.4.6",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.3.1",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.3.4",
|
||||
"vite-tsconfig-paths": "^4.3.2"
|
||||
}
|
||||
}
|
||||
|
11847
ui/pnpm-lock.yaml
11847
ui/pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -1,34 +1,13 @@
|
||||
import "./App.css";
|
||||
import { open } from "@tauri-apps/plugin-shell";
|
||||
|
||||
import { useState, ReactElement, useEffect } from "react";
|
||||
import { useState, ReactElement } from "react";
|
||||
import { useStore } from "@/state/store";
|
||||
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import {
|
||||
SettingsIcon,
|
||||
CircleHelpIcon,
|
||||
KeyRoundIcon,
|
||||
LogOutIcon,
|
||||
} from "lucide-react";
|
||||
import { KeyRoundIcon } from "lucide-react";
|
||||
import { Icon } from "@iconify/react";
|
||||
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||
|
||||
import {
|
||||
HomeIcon,
|
||||
ClockIcon,
|
||||
WrenchScrewdriverIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
||||
import { ChevronRightSquare } from "lucide-react";
|
||||
|
||||
import Logo from "./assets/logo-light.svg";
|
||||
|
||||
function classNames(...classes: any) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
import Home from "./pages/Home.tsx";
|
||||
import History from "./pages/History.tsx";
|
||||
import Dotfiles from "./pages/Dotfiles.tsx";
|
||||
@ -41,7 +20,6 @@ import {
|
||||
Button,
|
||||
ScrollShadow,
|
||||
Spacer,
|
||||
Tooltip,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
@ -49,19 +27,10 @@ import {
|
||||
DropdownTrigger,
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
useDisclosure,
|
||||
Checkbox,
|
||||
Input,
|
||||
Link,
|
||||
} from "@nextui-org/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { sectionItems } from "@/components/Sidebar/sidebar-items";
|
||||
import Sidebar, { SidebarItem } from "@/components/Sidebar";
|
||||
import icon from "@/assets/icon.svg";
|
||||
import iconText from "@/assets/logo-light.svg";
|
||||
import { logout } from "./state/client.ts";
|
||||
|
||||
enum Section {
|
||||
@ -89,8 +58,8 @@ function App() {
|
||||
// I think hashrouter may work, but I'd rather avoiding thinking of them as
|
||||
// pages
|
||||
const [section, setSection] = useState(Section.Home);
|
||||
const user = useStore((state) => state.user);
|
||||
const refreshUser = useStore((state) => state.refreshUser);
|
||||
const user = useStore((state: any) => state.user);
|
||||
const refreshUser = useStore((state: any) => state.refreshUser);
|
||||
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||
|
||||
const navigation: SidebarItem[] = [
|
||||
|
@ -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,13 +27,18 @@ function msToTime(ms: number) {
|
||||
else return days + " Days";
|
||||
}
|
||||
|
||||
export default function HistoryRow({ h }: any) {
|
||||
export default function HistoryRow({ h, compact }: any) {
|
||||
return (
|
||||
<li
|
||||
key={h.id}
|
||||
className="relative flex justify-between gap-x-6 px-4 py-5 hover:bg-gray-50 sm:px-6"
|
||||
className={cn(
|
||||
"relative flex justify-between gap-x-6 px-4 py-5 hover:bg-gray-50 sm:px-6",
|
||||
{ "py-5": !compact },
|
||||
{ "py-1": compact },
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 gap-x-4">
|
||||
{!compact && (
|
||||
<div className="flex flex-col justify-center">
|
||||
<p className="flex text-xs text-gray-500 justify-center">
|
||||
{DateTime.fromMillis(h.timestamp / 1000000).toLocaleString(
|
||||
@ -45,6 +51,7 @@ export default function HistoryRow({ h }: any) {
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-0 flex-col justify-center truncate">
|
||||
<Highlight
|
||||
theme={themes.github}
|
||||
|
1
ui/src/components/home/QuickActions.tsx
Normal file
1
ui/src/components/home/QuickActions.tsx
Normal file
@ -0,0 +1 @@
|
||||
export default function QuickActions() {}
|
@ -1,12 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
Input,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
Divider,
|
||||
Tooltip,
|
||||
Listbox,
|
||||
ListboxItem,
|
||||
@ -46,13 +41,13 @@ const NoteSidebar = () => {
|
||||
<div className="overflow-y-auto flex-grow">
|
||||
<Listbox
|
||||
hideSelectedIcon
|
||||
items={runbooks.map((runbook) => {
|
||||
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={
|
||||
<ButtonGroup className="z-20">
|
||||
@ -74,7 +69,7 @@ const NoteSidebar = () => {
|
||||
</ButtonGroup>
|
||||
}
|
||||
>
|
||||
{([runbook, info]) => (
|
||||
{([runbook, info]: [Runbook, { ptys: number }]) => (
|
||||
<ListboxItem
|
||||
key={runbook.id}
|
||||
onPress={() => {
|
||||
@ -124,7 +119,7 @@ const NoteSidebar = () => {
|
||||
<div className="text-xs text-gray-500">
|
||||
<em>
|
||||
{DateTime.fromJSDate(runbook.updated).toLocaleString(
|
||||
DateTime.DATETIME_SIMPLE,
|
||||
DateTime.DATETIME_SHORT,
|
||||
)}
|
||||
</em>
|
||||
</div>
|
||||
|
@ -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<Runbook | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@ -76,11 +88,21 @@ export default function Editor() {
|
||||
fetchRunbook();
|
||||
}, [runbookId]);
|
||||
|
||||
const editor = useMemo(() => {
|
||||
if (!runbook) {
|
||||
return undefined;
|
||||
}
|
||||
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),
|
||||
@ -91,28 +113,18 @@ export default function Editor() {
|
||||
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
|
||||
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 (
|
||||
<div className="flex w-full h-full flex-col justify-center items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (editor === undefined) {
|
||||
return (
|
||||
<div className="flex w-full h-full flex-col justify-center items-center">
|
||||
@ -139,7 +159,7 @@ export default function Editor() {
|
||||
>
|
||||
<SuggestionMenuController
|
||||
triggerCharacter={"/"}
|
||||
getItems={async (query) =>
|
||||
getItems={async (query: any) =>
|
||||
filterSuggestionItems(
|
||||
[...getDefaultReactSlashMenuItems(editor), insertRun(editor)],
|
||||
query,
|
||||
@ -148,7 +168,7 @@ export default function Editor() {
|
||||
/>
|
||||
|
||||
<SideMenuController
|
||||
sideMenu={(props) => (
|
||||
sideMenu={(props: any) => (
|
||||
<SideMenu {...props}>
|
||||
<AddBlockButton {...props} />
|
||||
<DeleteBlock {...props} />
|
||||
|
@ -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<string>("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: "" },
|
||||
});
|
||||
};
|
||||
|
||||
|
79
ui/src/components/ui/card.tsx
Normal file
79
ui/src/components/ui/card.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
363
ui/src/components/ui/chart.tsx
Normal file
363
ui/src/components/ui/chart.tsx
Normal file
@ -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<keyof typeof THEMES, string> }
|
||||
)
|
||||
}
|
||||
|
||||
type ChartContextProps = {
|
||||
config: ChartConfig
|
||||
}
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useChart must be used within a <ChartContainer />")
|
||||
}
|
||||
|
||||
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 (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
data-chart={chartId}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>
|
||||
{children}
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
)
|
||||
})
|
||||
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 (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Object.entries(THEMES)
|
||||
.map(
|
||||
([theme, prefix]) => `
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||
itemConfig.color
|
||||
return color ? ` --color-${key}: ${color};` : null
|
||||
})
|
||||
.join("\n")}
|
||||
}
|
||||
`
|
||||
)
|
||||
.join("\n"),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
||||
|
||||
const ChartTooltipContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean
|
||||
hideIndicator?: boolean
|
||||
indicator?: "line" | "dot" | "dashed"
|
||||
nameKey?: string
|
||||
labelKey?: string
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = "dot",
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { config } = useChart()
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [item] = payload
|
||||
const key = `${labelKey || item.dataKey || item.name || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
? config[label as keyof typeof config]?.label || label
|
||||
: itemConfig?.label
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn("font-medium", labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
payload,
|
||||
hideLabel,
|
||||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
])
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot"
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const indicatorColor = color || item.payload.fill || item.color
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
||||
indicator === "dot" && "items-center"
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn(
|
||||
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
|
||||
{
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
}
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--color-bg": indicatorColor,
|
||||
"--color-border": indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 justify-between leading-none",
|
||||
nestLabel ? "items-end" : "items-center"
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">
|
||||
{itemConfig?.label || item.name}
|
||||
</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
<span className="font-mono font-medium tabular-nums text-foreground">
|
||||
{item.value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
ChartTooltipContent.displayName = "ChartTooltip"
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend
|
||||
|
||||
const ChartLegendContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean
|
||||
nameKey?: string
|
||||
}
|
||||
>(
|
||||
(
|
||||
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
|
||||
ref
|
||||
) => {
|
||||
const { config } = useChart()
|
||||
|
||||
if (!payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-4",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{payload.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
ChartLegendContent.displayName = "ChartLegend"
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(
|
||||
config: ChartConfig,
|
||||
payload: unknown,
|
||||
key: string
|
||||
) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
"payload" in payload &&
|
||||
typeof payload.payload === "object" &&
|
||||
payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined
|
||||
|
||||
let configLabelKey: string = key
|
||||
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key as keyof typeof payload] === "string"
|
||||
) {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||
) {
|
||||
configLabelKey = payloadPayload[
|
||||
key as keyof typeof payloadPayload
|
||||
] as string
|
||||
}
|
||||
|
||||
return configLabelKey in config
|
||||
? config[configLabelKey]
|
||||
: config[key as keyof typeof config]
|
||||
}
|
||||
|
||||
export {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { NextUIProvider, Spacer } from "@nextui-org/react";
|
||||
import { NextUIProvider } from "@nextui-org/react";
|
||||
import App from "./App";
|
||||
import "./styles.css";
|
||||
|
||||
|
@ -2,32 +2,94 @@ import React, { useEffect } from "react";
|
||||
import { formatRelative } from "date-fns";
|
||||
import { Tooltip as ReactTooltip } from "react-tooltip";
|
||||
|
||||
import { useStore } from "@/state/store";
|
||||
import { AtuinState, useStore } from "@/state/store";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { ToastAction } from "@/components/ui/toast";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Listbox,
|
||||
ListboxItem,
|
||||
} from "@nextui-org/react";
|
||||
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
LabelList,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import { ChartConfig, ChartContainer } from "@/components/ui/chart";
|
||||
|
||||
import { Clock, Terminal } from "lucide-react";
|
||||
|
||||
import ActivityCalendar from "react-activity-calendar";
|
||||
import HistoryRow from "@/components/history/HistoryRow";
|
||||
import { ShellHistory } from "@/state/models";
|
||||
|
||||
function Stats({ stats }: any) {
|
||||
function StatCard({ name, stat }: any) {
|
||||
return (
|
||||
<div>
|
||||
<dl className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
|
||||
{stats.map((item: any) => (
|
||||
<div
|
||||
key={item.name}
|
||||
className="overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6"
|
||||
<Card shadow="sm">
|
||||
<CardHeader>
|
||||
<h3 className="uppercase text-gray-500">{name}</h3>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<h2 className="font-bold text-xl">{stat}</h2>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function TopChart({ chartData }: any) {
|
||||
const chartConfig = {
|
||||
command: {
|
||||
label: "Command",
|
||||
color: "#c4edde",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
return (
|
||||
<ChartContainer config={chartConfig} className="max-h-72">
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
layout="vertical"
|
||||
margin={{
|
||||
right: 16,
|
||||
}}
|
||||
>
|
||||
<dt className="truncate text-sm font-medium text-gray-500">
|
||||
{item.name}
|
||||
</dt>
|
||||
<dd className="mt-1 text-xl font-semibold tracking-tight text-gray-900">
|
||||
{item.stat}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
<CartesianGrid horizontal={false} />
|
||||
<YAxis
|
||||
dataKey="command"
|
||||
type="category"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
hide
|
||||
/>
|
||||
<XAxis dataKey="count" type="number" hide />
|
||||
<Bar dataKey="count" layout="vertical" fill="#c4edde" radius={4}>
|
||||
<LabelList
|
||||
dataKey="command"
|
||||
position="insideLeft"
|
||||
offset={8}
|
||||
className="fill-[--color-label]"
|
||||
fontSize={12}
|
||||
/>
|
||||
<LabelList
|
||||
dataKey="count"
|
||||
position="right"
|
||||
offset={8}
|
||||
className="fill-foreground"
|
||||
fontSize={12}
|
||||
/>
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -62,14 +124,22 @@ const explicitTheme = {
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
const homeInfo = useStore((state) => state.homeInfo);
|
||||
const user = useStore((state) => state.user);
|
||||
const calendar = useStore((state) => state.calendar);
|
||||
const weekStart = useStore((state) => state.weekStart);
|
||||
const homeInfo = useStore((state: AtuinState) => state.homeInfo);
|
||||
const user = useStore((state: AtuinState) => state.user);
|
||||
const calendar = useStore((state: AtuinState) => state.calendar);
|
||||
const runbooks = useStore((state: AtuinState) => state.runbooks);
|
||||
const weekStart = useStore((state: AtuinState) => state.weekStart);
|
||||
|
||||
const refreshHomeInfo = useStore((state) => state.refreshHomeInfo);
|
||||
const refreshUser = useStore((state) => state.refreshUser);
|
||||
const refreshCalendar = useStore((state) => state.refreshCalendar);
|
||||
const refreshHomeInfo = useStore(
|
||||
(state: AtuinState) => state.refreshHomeInfo,
|
||||
);
|
||||
const refreshUser = useStore((state: AtuinState) => state.refreshUser);
|
||||
const refreshCalendar = useStore(
|
||||
(state: AtuinState) => state.refreshCalendar,
|
||||
);
|
||||
const refreshRunbooks = useStore(
|
||||
(state: AtuinState) => state.refreshRunbooks,
|
||||
);
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
@ -77,6 +147,9 @@ export default function Home() {
|
||||
refreshHomeInfo();
|
||||
refreshUser();
|
||||
refreshCalendar();
|
||||
refreshRunbooks();
|
||||
|
||||
console.log(homeInfo);
|
||||
|
||||
let setup = async () => {
|
||||
let installed = await invoke("is_cli_installed");
|
||||
@ -125,34 +198,39 @@ export default function Home() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full flex-1 flex-col p-4">
|
||||
<div className="p-10">
|
||||
<div className="w-full flex-1 flex-col p-4 overflow-y-auto">
|
||||
<div className="pl-10">
|
||||
<Header name={user.username} />
|
||||
|
||||
<div className="pt-10">
|
||||
<Stats
|
||||
stats={[
|
||||
{
|
||||
name: "Last Sync",
|
||||
stat:
|
||||
</div>
|
||||
<div className="p-10 grid grid-cols-4 gap-4">
|
||||
<StatCard
|
||||
name="Last Sync"
|
||||
stat={
|
||||
(homeInfo.lastSyncTime &&
|
||||
formatRelative(homeInfo.lastSyncTime, new Date())) ||
|
||||
"Never",
|
||||
},
|
||||
{
|
||||
name: "Total history records",
|
||||
stat: homeInfo.historyCount.toLocaleString(),
|
||||
},
|
||||
{
|
||||
name: "Other records",
|
||||
stat: homeInfo.recordCount - homeInfo.historyCount,
|
||||
},
|
||||
]}
|
||||
"Never"
|
||||
}
|
||||
/>
|
||||
<StatCard
|
||||
name="Total Commands"
|
||||
stat={homeInfo.historyCount.toLocaleString()}
|
||||
/>
|
||||
<StatCard
|
||||
name="Total Runbooks"
|
||||
stat={runbooks.length.toLocaleString()}
|
||||
/>
|
||||
<StatCard
|
||||
name="Other Records"
|
||||
stat={homeInfo.recordCount - homeInfo.historyCount}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pt-10 flex justify-around">
|
||||
<Card shadow="sm" className="col-span-3">
|
||||
<CardHeader>
|
||||
<h2 className="uppercase text-gray-500">Activity graph</h2>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<ActivityCalendar
|
||||
hideTotalCount
|
||||
theme={explicitTheme}
|
||||
data={calendar}
|
||||
weekStart={weekStart as any}
|
||||
@ -162,12 +240,55 @@ export default function Home() {
|
||||
"data-tooltip-html": `${activity.count} commands on ${activity.date}`,
|
||||
})
|
||||
}
|
||||
labels={{
|
||||
totalCount: "{{count}} history records in the last year",
|
||||
}}
|
||||
/>
|
||||
<ReactTooltip id="react-tooltip" />
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Card shadow="sm">
|
||||
<CardHeader>
|
||||
<h2 className="uppercase text-gray-500">Quick actions </h2>
|
||||
</CardHeader>
|
||||
|
||||
<CardBody>
|
||||
<Listbox variant="flat" aria-label="Quick actions">
|
||||
<ListboxItem
|
||||
key="new-runbook"
|
||||
description="Create an executable runbook"
|
||||
startContent={<Terminal />}
|
||||
>
|
||||
New runbook
|
||||
</ListboxItem>
|
||||
<ListboxItem
|
||||
key="shell-history"
|
||||
description="Search and explore shell history"
|
||||
startContent={<Clock />}
|
||||
>
|
||||
Shell History
|
||||
</ListboxItem>
|
||||
</Listbox>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Card shadow="sm" className="col-span-2">
|
||||
<CardHeader>
|
||||
<h2 className="uppercase text-gray-500">Recent commands</h2>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{homeInfo.recentCommands?.map((i: ShellHistory) => {
|
||||
return <HistoryRow compact h={i} />;
|
||||
})}
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Card shadow="sm" className="col-span-2">
|
||||
<CardHeader>
|
||||
<h2 className="uppercase text-gray-500">Top commands</h2>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<TopChart chartData={homeInfo.topCommands} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -18,12 +18,16 @@ export interface HomeInfo {
|
||||
historyCount: number;
|
||||
recordCount: number;
|
||||
lastSyncTime: Date | null;
|
||||
recentCommands: ShellHistory[];
|
||||
topCommands: ShellHistory[];
|
||||
}
|
||||
|
||||
export const DefaultHomeInfo: HomeInfo = {
|
||||
historyCount: 0,
|
||||
recordCount: 0,
|
||||
lastSyncTime: new Date(),
|
||||
recentCommands: [],
|
||||
topCommands: [],
|
||||
};
|
||||
|
||||
export class ShellHistory {
|
||||
|
@ -58,7 +58,7 @@ export interface AtuinState {
|
||||
|
||||
setCurrentRunbook: (id: String) => void;
|
||||
setPtyTerm: (pty: string, terminal: any) => void;
|
||||
newPtyTerm: (pty: string, runbook: string) => TerminalData;
|
||||
newPtyTerm: (pty: string) => TerminalData;
|
||||
cleanupPtyTerm: (pty: string) => void;
|
||||
|
||||
terminals: { [pty: string]: TerminalData };
|
||||
@ -125,11 +125,17 @@ let state = (set: any, get: any): AtuinState => ({
|
||||
refreshHomeInfo: () => {
|
||||
invoke("home_info")
|
||||
.then((res: any) => {
|
||||
console.log(res);
|
||||
set({
|
||||
homeInfo: {
|
||||
historyCount: res.history_count,
|
||||
recordCount: res.record_count,
|
||||
lastSyncTime: (res.last_sync && parseISO(res.last_sync)) || null,
|
||||
recentCommands: res.recent_commands,
|
||||
topCommands: res.top_commands.map((top: any) => ({
|
||||
command: top[0],
|
||||
count: top[1],
|
||||
})),
|
||||
},
|
||||
});
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user