diff options
Diffstat (limited to 'ui/src')
| -rw-r--r-- | ui/src/App.css | 8 | ||||
| -rw-r--r-- | ui/src/App.tsx | 18 | ||||
| -rw-r--r-- | ui/src/components/Drawer.tsx | 2 | ||||
| -rw-r--r-- | ui/src/components/HistoryList.tsx | 131 | ||||
| -rw-r--r-- | ui/src/components/dotfiles/Aliases.tsx | 37 | ||||
| -rw-r--r-- | ui/src/components/history/Stats.tsx | 56 | ||||
| -rw-r--r-- | ui/src/pages/Dotfiles.tsx | 7 | ||||
| -rw-r--r-- | ui/src/pages/History.tsx | 45 | ||||
| -rw-r--r-- | ui/src/pages/Home.tsx | 84 | ||||
| -rw-r--r-- | ui/src/state/models.ts | 34 | ||||
| -rw-r--r-- | ui/src/state/store.ts | 72 |
11 files changed, 306 insertions, 188 deletions
diff --git a/ui/src/App.css b/ui/src/App.css index a89ebd15..5a32a1a5 100644 --- a/ui/src/App.css +++ b/ui/src/App.css @@ -1,7 +1,11 @@ +html { + overscroll-behavior: none; +} + .logo.vite:hover { - filter: drop-shadow(0 0 2em #747bff); + filter: drop-shadow(0 0 2em #747bff); } .logo.react:hover { - filter: drop-shadow(0 0 2em #61dafb); + filter: drop-shadow(0 0 2em #61dafb); } diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 5d1cd863..ae6ebdb1 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,14 +1,9 @@ import "./App.css"; -import { Fragment, useState, useEffect, ReactElement } from "react"; -import { Dialog, Transition } from "@headlessui/react"; +import { useState, ReactElement } from "react"; import { - Bars3Icon, - ChartPieIcon, Cog6ToothIcon, HomeIcon, - XMarkIcon, - MagnifyingGlassIcon, ClockIcon, WrenchScrewdriverIcon, } from "@heroicons/react/24/outline"; @@ -18,16 +13,20 @@ 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"; enum Section { + Home, History, Dotfiles, } function renderMain(section: Section): ReactElement { switch (section) { + case Section.Home: + return <Home />; case Section.History: return <History />; case Section.Dotfiles: @@ -39,10 +38,15 @@ function App() { // routers don't really work in Tauri. It's not a browser! // I think hashrouter may work, but I'd rather avoiding thinking of them as // pages - const [section, setSection] = useState(Section.History); + const [section, setSection] = useState(Section.Home); const navigation = [ { + name: "Home", + icon: HomeIcon, + section: Section.Home, + }, + { name: "History", icon: ClockIcon, section: Section.History, diff --git a/ui/src/components/Drawer.tsx b/ui/src/components/Drawer.tsx index 65bb5ab4..91753624 100644 --- a/ui/src/components/Drawer.tsx +++ b/ui/src/components/Drawer.tsx @@ -1,5 +1,3 @@ -import * as React from "react"; - import { Drawer as VDrawer } from "vaul"; export default function Drawer({ diff --git a/ui/src/components/HistoryList.tsx b/ui/src/components/HistoryList.tsx index b31a4be4..9616ecf0 100644 --- a/ui/src/components/HistoryList.tsx +++ b/ui/src/components/HistoryList.tsx @@ -1,75 +1,88 @@ -import { DateTime } from 'luxon'; -import { ChevronRightIcon } from '@heroicons/react/20/solid' +import { ChevronRightIcon } from "@heroicons/react/20/solid"; -function msToTime(ms) { - let milliseconds = (ms).toFixed(1); - let seconds = (ms / 1000).toFixed(1); - let minutes = (ms / (1000 * 60)).toFixed(1); - let hours = (ms / (1000 * 60 * 60)).toFixed(1); - let days = (ms / (1000 * 60 * 60 * 24)).toFixed(1); +// @ts-ignore +import { DateTime } from "luxon"; + +function msToTime(ms: number) { + let milliseconds = parseInt(ms.toFixed(1)); + let seconds = parseInt((ms / 1000).toFixed(1)); + let minutes = parseInt((ms / (1000 * 60)).toFixed(1)); + let hours = parseInt((ms / (1000 * 60 * 60)).toFixed(1)); + let days = parseInt((ms / (1000 * 60 * 60 * 24)).toFixed(1)); if (milliseconds < 1000) return milliseconds + "ms"; else if (seconds < 60) return seconds + "s"; else if (minutes < 60) return minutes + "m"; else if (hours < 24) return hours + "hr"; - else return days + " Days" + else return days + " Days"; } -export default function HistoryList(props){ +export default function HistoryList(props: any) { return ( + <ul + role="list" + className="divide-y divide-gray-100 overflow-hidden bg-white shadow-sm ring-1 ring-gray-900/5" + > + {props.history.map((h: any) => ( + <li + key={h.id} + className="relative flex justify-between gap-x-6 px-4 py-5 hover:bg-gray-50 sm:px-6" + > + <div className="flex min-w-0 gap-x-4"> + <div className="flex flex-col justify-center"> + <p className="flex text-xs text-gray-500 justify-center"> + {DateTime.fromMillis(h.timestamp / 1000000).toLocaleString( + DateTime.TIME_WITH_SECONDS, + )} + </p> + <p className="flex text-xs mt-1 text-gray-400 justify-center"> + {DateTime.fromMillis(h.timestamp / 1000000).toLocaleString( + DateTime.DATE_SHORT, + )} + </p> + </div> + <div className="min-w-0 flex-col justify-center"> + <pre className="whitespace-pre-wrap"> + <code className="text-sm">{h.command}</code> + </pre> + <p className="mt-1 flex text-xs leading-5 text-gray-500"> + <span className="relative truncate ">{h.user}</span> - <ul - role="list" - className="divide-y divide-gray-100 overflow-hidden bg-white shadow-sm ring-1 ring-gray-900/5" - > - {props.history.map((h) => ( - <li key={h.id} className="relative flex justify-between gap-x-6 px-4 py-5 hover:bg-gray-50 sm:px-6"> - <div className="flex min-w-0 gap-x-4"> - <div className="flex flex-col justify-center"> - <p className="flex text-xs text-gray-500 justify-center">{ DateTime.fromMillis(h.timestamp / 1000000).toLocaleString(DateTime.TIME_WITH_SECONDS)}</p> - <p className="flex text-xs mt-1 text-gray-400 justify-center">{ DateTime.fromMillis(h.timestamp / 1000000).toLocaleString(DateTime.DATE_SHORT)}</p> - </div> - <div className="min-w-0 flex-col justify-center"> - <pre className="whitespace-pre-wrap"><code className="text-sm">{h.command}</code></pre> - <p className="mt-1 flex text-xs leading-5 text-gray-500"> - <span className="relative truncate "> - {h.user} - </span> - - <span> on </span> + <span> on </span> - <span className="relative truncate "> - {h.host} - </span> + <span className="relative truncate ">{h.host}</span> - <span> in </span> + <span> in </span> - <span className="relative truncate "> - {h.cwd} - </span> - </p> - </div> - </div> - <div className="flex shrink-0 items-center gap-x-4"> - <div className="hidden sm:flex sm:flex-col sm:items-end"> - <p className="text-sm leading-6 text-gray-900">{h.exit}</p> - {h.duration ? ( - <p className="mt-1 text-xs leading-5 text-gray-500"> - <time dateTime={h.duration}>{msToTime(h.duration / 1000000)}</time> - </p> - ) : ( - <div className="mt-1 flex items-center gap-x-1.5"> - <div className="flex-none rounded-full bg-emerald-500/20 p-1"> - <div className="h-1.5 w-1.5 rounded-full bg-emerald-500" /> - </div> - <p className="text-xs leading-5 text-gray-500">Online</p> - </div> - )} - </div> - <ChevronRightIcon className="h-5 w-5 flex-none text-gray-400" aria-hidden="true" /> + <span className="relative truncate ">{h.cwd}</span> + </p> + </div> + </div> + <div className="flex shrink-0 items-center gap-x-4"> + <div className="hidden sm:flex sm:flex-col sm:items-end"> + <p className="text-sm leading-6 text-gray-900">{h.exit}</p> + {h.duration ? ( + <p className="mt-1 text-xs leading-5 text-gray-500"> + <time dateTime={h.duration}> + {msToTime(h.duration / 1000000)} + </time> + </p> + ) : ( + <div className="mt-1 flex items-center gap-x-1.5"> + <div className="flex-none rounded-full bg-emerald-500/20 p-1"> + <div className="h-1.5 w-1.5 rounded-full bg-emerald-500" /> </div> - </li> - ))} - </ul> + <p className="text-xs leading-5 text-gray-500">Online</p> + </div> + )} + </div> + <ChevronRightIcon + className="h-5 w-5 flex-none text-gray-400" + aria-hidden="true" + /> + </div> + </li> + ))} + </ul> ); } diff --git a/ui/src/components/dotfiles/Aliases.tsx b/ui/src/components/dotfiles/Aliases.tsx index 4854e6b5..61fd001c 100644 --- a/ui/src/components/dotfiles/Aliases.tsx +++ b/ui/src/components/dotfiles/Aliases.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import DataTable from "@/components/ui/data-table"; import { Button } from "@/components/ui/button"; @@ -8,34 +8,21 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, - DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { ColumnDef } from "@tanstack/react-table"; + import { invoke } from "@tauri-apps/api/core"; import Drawer from "@/components/Drawer"; -function loadAliases( - setAliases: React.Dispatch<React.SetStateAction<never[]>>, -) { - invoke("aliases").then((aliases: any) => { - setAliases(aliases); - }); -} - -type Alias = { - name: string; - value: string; -}; +import { Alias } from "@/state/models"; +import { useStore } from "@/state/store"; -function deleteAlias( - name: string, - setAliases: React.Dispatch<React.SetStateAction<never[]>>, -) { +function deleteAlias(name: string, refreshAliases: () => void) { invoke("delete_alias", { name: name }) .then(() => { - console.log("Deleted alias"); - loadAliases(setAliases); + refreshAliases(); }) .catch(() => { console.error("Failed to delete alias"); @@ -101,7 +88,9 @@ function AddAlias({ onAdd: onAdd }: { onAdd?: () => void }) { } export default function Aliases() { - let [aliases, setAliases] = useState([]); + const aliases = useStore((state) => state.aliases); + const refreshAliases = useStore((state) => state.refreshAliases); + let [aliasDrawerOpen, setAliasDrawerOpen] = useState(false); const columns: ColumnDef<Alias>[] = [ @@ -129,7 +118,7 @@ export default function Aliases() { <DropdownMenuContent align="end"> <DropdownMenuLabel>Actions</DropdownMenuLabel> <DropdownMenuItem - onClick={() => deleteAlias(alias.name, setAliases)} + onClick={() => deleteAlias(alias.name, refreshAliases)} > Delete </DropdownMenuItem> @@ -141,7 +130,7 @@ export default function Aliases() { ]; useEffect(() => { - loadAliases(setAliases); + refreshAliases(); }, []); return ( @@ -172,7 +161,7 @@ export default function Aliases() { > <AddAlias onAdd={() => { - loadAliases(setAliases); + refreshAliases(); setAliasDrawerOpen(false); }} /> diff --git a/ui/src/components/history/Stats.tsx b/ui/src/components/history/Stats.tsx index afd9ed89..ce92ac04 100644 --- a/ui/src/components/history/Stats.tsx +++ b/ui/src/components/history/Stats.tsx @@ -5,29 +5,18 @@ import PacmanLoader from "react-spinners/PacmanLoader"; import { BarChart, Bar, - Rectangle, XAxis, YAxis, - CartesianGrid, Tooltip, - Legend, ResponsiveContainer, } from "recharts"; -const tabs = [ - { name: "Daily", href: "#", current: true }, - { name: "Weekly", href: "#", current: false }, - { name: "Monthly", href: "#", current: false }, -]; - -function classNames(...classes) { - return classes.filter(Boolean).join(" "); -} - function renderLoading() { - <div className="flex items-center justify-center h-full"> - <PacmanLoader color="#26bd65" /> - </div>; + return ( + <div className="flex items-center justify-center h-full"> + <PacmanLoader color="#26bd65" /> + </div> + ); } export default function Stats() { @@ -77,7 +66,7 @@ export default function Stats() { <div className="flex flex-col"> <div className="flexfull"> <dl className="grid grid-cols-1 sm:grid-cols-4 w-full"> - {stats.map((item) => ( + {stats.map((item: any) => ( <div key={item.name} className="overflow-hidden bg-white px-4 py-5 shadow sm:p-6" @@ -94,39 +83,6 @@ export default function Stats() { </div> <div className="flex flex-col h-54 py-4 pl-5"> - <div className="sm:hidden"> - {/* Use an "onChange" listener to redirect the user to the selected tab URL. */} - <select - id="tabs" - name="tabs" - className="block w-full rounded-md border-gray-300 focus:border-green-500 focus:ring-green-500" - defaultValue={tabs.find((tab) => tab.current).name} - > - {tabs.map((tab) => ( - <option key={tab.name}>{tab.name}</option> - ))} - </select> - </div> - <div className="hidden sm:block"> - <nav className="flex space-x-4" aria-label="Tabs"> - {tabs.map((tab) => ( - <a - key={tab.name} - href={tab.href} - className={classNames( - tab.current - ? "bg-gray-100 text-gray-700" - : "text-gray-500 hover:text-gray-700", - "rounded-md px-3 py-2 text-sm font-medium", - )} - aria-current={tab.current ? "page" : undefined} - > - {tab.name} - </a> - ))} - </nav> - </div> - <div className="flex flex-col h-48 pt-5 pr-5"> <ResponsiveContainer width="100%" height="100%"> <BarChart width={500} height={300} data={chart}> diff --git a/ui/src/pages/Dotfiles.tsx b/ui/src/pages/Dotfiles.tsx index bd209062..6b0870b3 100644 --- a/ui/src/pages/Dotfiles.tsx +++ b/ui/src/pages/Dotfiles.tsx @@ -1,12 +1,5 @@ -import { useState } from "react"; - -import { Cog6ToothIcon } from "@heroicons/react/24/outline"; - import Aliases from "@/components/dotfiles/Aliases"; -import { Drawer } from "@/components/drawer"; -import { invoke } from "@tauri-apps/api/core"; - function Header() { return ( <div className="md:flex md:items-center md:justify-between"> diff --git a/ui/src/pages/History.tsx b/ui/src/pages/History.tsx index f74c16ac..91ed9824 100644 --- a/ui/src/pages/History.tsx +++ b/ui/src/pages/History.tsx @@ -1,40 +1,10 @@ -import { Fragment, useState, useEffect } from "react"; -import { Dialog, Transition } from "@headlessui/react"; -import { - Bars3Icon, - ChartPieIcon, - Cog6ToothIcon, - HomeIcon, - XMarkIcon, -} from "@heroicons/react/24/outline"; - -import Logo from "../assets/logo-light.svg"; - -import { invoke } from "@tauri-apps/api/core"; +import { useEffect } from "react"; import HistoryList from "@/components/HistoryList.tsx"; import HistorySearch from "@/components/HistorySearch.tsx"; import Stats from "@/components/history/Stats.tsx"; import Drawer from "@/components/Drawer.tsx"; - -function refreshHistory( - setHistory: React.Dispatch<React.SetStateAction<never[]>>, - query: String | null, -) { - if (query) { - invoke("search", { query: query }) - .then((res: any[]) => { - setHistory(res); - }) - .catch((e) => { - console.log(e); - }); - } else { - invoke("list").then((h: any[]) => { - setHistory(h); - }); - } -} +import { useStore } from "@/state/store"; function Header() { return ( @@ -44,7 +14,7 @@ function Header() { Shell History </h2> </div> - <div className="mt-4 flex md:ml-4 md:mt-0"> + <div className="flex"> <Drawer width="70%" trigger={ @@ -77,10 +47,11 @@ function Header() { } export default function Search() { - let [history, setHistory] = useState([]); + const history = useStore((state) => state.shellHistory); + const refreshHistory = useStore((state) => state.refreshShellHistory); useEffect(() => { - refreshHistory(setHistory, null); + refreshHistory(); }, []); return ( @@ -93,8 +64,8 @@ export default function Search() { <div className="flex h-16 shrink-0 items-center gap-x-4 border-b border-t border-gray-200 bg-white px-4 shadow-sm sm:gap-x-6 sm:px-6 lg:px-8"> <HistorySearch - refresh={(query: String | null) => { - refreshHistory(setHistory, query); + refresh={(query?: string) => { + refreshHistory(query); }} /> </div> diff --git a/ui/src/pages/Home.tsx b/ui/src/pages/Home.tsx new file mode 100644 index 00000000..c0f8fbc5 --- /dev/null +++ b/ui/src/pages/Home.tsx @@ -0,0 +1,84 @@ +import { useEffect } from "react"; +import { formatRelative } from "date-fns"; + +import { useStore } from "@/state/store"; + +function Stats({ stats }: 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" + > + <dt className="truncate text-sm font-medium text-gray-500"> + {item.name} + </dt> + <dd className="mt-1 text-3xl font-semibold tracking-tight text-gray-900"> + {item.stat} + </dd> + </div> + ))} + </dl> + </div> + ); +} + +function Header({ name }: any) { + let greeting = name && name.length > 0 ? "Hey, " + name + "!" : "Hey!"; + + return ( + <div className="md:flex md:items-center md:justify-between"> + <div className="min-w-0 flex-1"> + <h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight"> + {greeting} + </h2> + <h3 className="text-xl leading-7 text-gray-900 pt-4"> + Welcome to Atuin. + </h3> + </div> + </div> + ); +} + +export default function Home() { + const homeInfo = useStore((state) => state.homeInfo); + const refreshHomeInfo = useStore((state) => state.refreshHomeInfo); + + useEffect(() => { + refreshHomeInfo(); + }, []); + + if (!homeInfo) { + return <div>Loading...</div>; + } + + return ( + <div className="pl-60"> + <div className="p-10"> + <Header name={"Ellie"} /> + + <div className="pt-10"> + <h2 className="text-xl font-bold">Sync</h2> + <Stats + stats={[ + { + name: "Last Sync", + stat: formatRelative(homeInfo.lastSyncTime, new Date()), + }, + { + name: "Total history records", + stat: homeInfo.historyCount.toLocaleString(), + }, + { + name: "Other records", + stat: homeInfo.recordCount - homeInfo.historyCount, + }, + ]} + /> + </div> + </div> + </div> + ); +} diff --git a/ui/src/state/models.ts b/ui/src/state/models.ts new file mode 100644 index 00000000..f11ce651 --- /dev/null +++ b/ui/src/state/models.ts @@ -0,0 +1,34 @@ +export interface User { + username: string; +} + +export const DefaultUser: User = { + username: "", +}; + +export interface HomeInfo { + historyCount: number; + recordCount: number; + lastSyncTime: Date; +} + +export const DefaultHomeInfo: HomeInfo = { + historyCount: 0, + recordCount: 0, + lastSyncTime: new Date(), +}; + +export interface ShellHistory { + id: string; + timestamp: number; + command: string; + user: string; + host: string; + cwd: string; + duration: number; +} + +export interface Alias { + name: string; + value: string; +} diff --git a/ui/src/state/store.ts b/ui/src/state/store.ts new file mode 100644 index 00000000..08410ba8 --- /dev/null +++ b/ui/src/state/store.ts @@ -0,0 +1,72 @@ +import { create } from "zustand"; +import { parseISO } from "date-fns"; + +import { + User, + DefaultUser, + HomeInfo, + DefaultHomeInfo, + Alias, + ShellHistory, +} from "./models"; + +import { invoke } from "@tauri-apps/api/core"; + +// I'll probs want to slice this up at some point, but for now a +// big blobby lump of state is fine. +// Totally just hoping that structure will be emergent in the future. +interface AtuinState { + user: User; + homeInfo: HomeInfo; + aliases: Alias[]; + shellHistory: ShellHistory[]; + + refreshHomeInfo: () => void; + refreshAliases: () => void; + refreshShellHistory: (query?: string) => void; +} + +export const useStore = create<AtuinState>()((set) => ({ + user: DefaultUser, + homeInfo: DefaultHomeInfo, + aliases: [], + shellHistory: [], + + refreshAliases: () => { + invoke("aliases").then((aliases: any) => { + set({ aliases: aliases }); + }); + }, + + refreshShellHistory: (query?: string) => { + if (query) { + invoke("search", { query: query }) + .then((res: any) => { + set({ shellHistory: res }); + }) + .catch((e) => { + console.log(e); + }); + } else { + invoke("list").then((res: any) => { + set({ shellHistory: res }); + }); + } + }, + + refreshHomeInfo: () => { + invoke("home_info") + .then((res: any) => { + set({ + homeInfo: { + historyCount: res.history_count, + recordCount: res.record_count, + lastSyncTime: parseISO(res.last_sync), + }, + }); + }) + .catch((e) => { + console.log(e); + }); + }, +})); |
