diff options
author | Ellie Huxtable <ellie@elliehuxtable.com> | 2024-04-17 14:06:05 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-04-17 14:06:05 +0100 |
commit | cb19925011d889c513e1bbedc446e399597e38a0 (patch) | |
tree | 7ad9e42013e15957805f2cdf563ce8b3e0c770f5 | |
parent | fcc0dc1bd576cd7b608e191c3a71e863d14b2d1f (diff) |
feat(gui): work on home page, sort state (#1956)
1. Start on a home page, can sort onboarding/etc from there
2. Introduce zustand for state management. It's nice!
Did a production build and clicked around for a while. Memory usage
seems nice and chill.
-rw-r--r-- | atuin-client/src/api_client.rs | 14 | ||||
-rw-r--r-- | atuin-client/src/history.rs | 2 | ||||
-rw-r--r-- | atuin-client/src/record/sqlite_store.rs | 10 | ||||
-rw-r--r-- | atuin-client/src/record/store.rs | 1 | ||||
-rw-r--r-- | ui/backend/Cargo.lock | 6 | ||||
-rw-r--r-- | ui/backend/src/main.rs | 61 | ||||
-rw-r--r-- | ui/package.json | 4 | ||||
-rw-r--r-- | ui/pnpm-lock.yaml | 38 | ||||
-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 |
19 files changed, 436 insertions, 194 deletions
diff --git a/atuin-client/src/api_client.rs b/atuin-client/src/api_client.rs index a19e5305..f31a796e 100644 --- a/atuin-client/src/api_client.rs +++ b/atuin-client/src/api_client.rs @@ -11,7 +11,7 @@ use reqwest::{ use atuin_common::{ api::{ AddHistoryRequest, ChangePasswordRequest, CountResponse, DeleteHistoryRequest, - ErrorResponse, LoginRequest, LoginResponse, RegisterResponse, StatusResponse, + ErrorResponse, LoginRequest, LoginResponse, MeResponse, RegisterResponse, StatusResponse, SyncHistoryResponse, }, record::RecordStatus, @@ -234,6 +234,18 @@ impl<'a> Client<'a> { Ok(status) } + pub async fn me(&self) -> Result<MeResponse> { + let url = format!("{}/api/v0/me", self.sync_addr); + let url = Url::parse(url.as_str())?; + + let resp = self.client.get(url).send().await?; + let resp = handle_resp_error(resp).await?; + + let status = resp.json::<MeResponse>().await?; + + Ok(status) + } + pub async fn get_history( &self, sync_ts: OffsetDateTime, diff --git a/atuin-client/src/history.rs b/atuin-client/src/history.rs index bc74aebd..1b590e88 100644 --- a/atuin-client/src/history.rs +++ b/atuin-client/src/history.rs @@ -18,7 +18,7 @@ mod builder; pub mod store; const HISTORY_VERSION: &str = "v0"; -const HISTORY_TAG: &str = "history"; +pub const HISTORY_TAG: &str = "history"; #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct HistoryId(pub String); diff --git a/atuin-client/src/record/sqlite_store.rs b/atuin-client/src/record/sqlite_store.rs index e17893eb..31de311b 100644 --- a/atuin-client/src/record/sqlite_store.rs +++ b/atuin-client/src/record/sqlite_store.rs @@ -180,6 +180,16 @@ impl Store for SqliteStore { self.idx(host, tag, 0).await } + async fn len_all(&self) -> Result<u64> { + let res: Result<(i64,), sqlx::Error> = sqlx::query_as("select count(*) from store") + .fetch_one(&self.pool) + .await; + match res { + Err(e) => Err(eyre!("failed to fetch local store len: {}", e)), + Ok(v) => Ok(v.0 as u64), + } + } + async fn len_tag(&self, tag: &str) -> Result<u64> { let res: Result<(i64,), sqlx::Error> = sqlx::query_as("select count(*) from store where tag=?1") diff --git a/atuin-client/src/record/store.rs b/atuin-client/src/record/store.rs index 188e18ce..49ca4968 100644 --- a/atuin-client/src/record/store.rs +++ b/atuin-client/src/record/store.rs @@ -25,6 +25,7 @@ pub trait Store { async fn delete(&self, id: RecordId) -> Result<()>; async fn delete_all(&self) -> Result<()>; + async fn len_all(&self) -> Result<u64>; async fn len(&self, host: HostId, tag: &str) -> Result<u64>; async fn len_tag(&self, tag: &str) -> Result<u64>; diff --git a/ui/backend/Cargo.lock b/ui/backend/Cargo.lock index 042e2c5a..abb72ce2 100644 --- a/ui/backend/Cargo.lock +++ b/ui/backend/Cargo.lock @@ -212,7 +212,7 @@ dependencies = [ [[package]] name = "atuin-client" -version = "18.1.0" +version = "18.2.0" dependencies = [ "async-trait", "atuin-common", @@ -259,7 +259,7 @@ dependencies = [ [[package]] name = "atuin-common" -version = "18.1.0" +version = "18.2.0" dependencies = [ "eyre", "lazy_static", @@ -276,7 +276,7 @@ dependencies = [ [[package]] name = "atuin-dotfiles" -version = "0.1.0" +version = "0.2.0" dependencies = [ "atuin-client", "atuin-common", diff --git a/ui/backend/src/main.rs b/ui/backend/src/main.rs index 98967562..fbcf9481 100644 --- a/ui/backend/src/main.rs +++ b/ui/backend/src/main.rs @@ -2,6 +2,7 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use std::path::PathBuf; +use time::format_description::well_known::Rfc3339; use atuin_client::settings::Settings; @@ -9,9 +10,20 @@ mod db; mod dotfiles; mod store; +use atuin_client::{ + encryption, history::HISTORY_TAG, record::sqlite_store::SqliteStore, record::store::Store, +}; use db::{GlobalStats, HistoryDB, UIHistory}; use dotfiles::aliases::aliases; +#[derive(Debug, serde::Serialize)] +struct HomeInfo { + pub username: String, + pub record_count: u64, + pub history_count: u64, + pub last_sync: String, +} + #[tauri::command] async fn list() -> Result<Vec<UIHistory>, String> { let settings = Settings::new().map_err(|e| e.to_string())?; @@ -47,6 +59,54 @@ async fn global_stats() -> Result<GlobalStats, String> { Ok(stats) } +#[tauri::command] +async fn home_info() -> Result<HomeInfo, String> { + let settings = Settings::new().map_err(|e| e.to_string())?; + let record_store_path = PathBuf::from(settings.record_store_path.as_str()); + let sqlite_store = SqliteStore::new(record_store_path, settings.local_timeout) + .await + .map_err(|e| e.to_string())?; + + let client = atuin_client::api_client::Client::new( + &settings.sync_address, + &settings.session_token, + settings.network_connect_timeout, + settings.network_timeout, + ) + .map_err(|e| e.to_string())?; + + let session_path = settings.session_path.as_str(); + let last_sync = Settings::last_sync() + .map_err(|e| e.to_string())? + .format(&Rfc3339) + .map_err(|e| e.to_string())?; + let record_count = sqlite_store.len_all().await.map_err(|e| e.to_string())?; + let history_count = sqlite_store + .len_tag(HISTORY_TAG) + .await + .map_err(|e| e.to_string())?; + + let info = if !PathBuf::from(session_path).exists() { + HomeInfo { + username: String::from(""), + last_sync: last_sync.to_string(), + record_count, + history_count, + } + } else { + let me = client.me().await.map_err(|e| e.to_string())?; + + HomeInfo { + username: me.username, + last_sync: last_sync.to_string(), + record_count, + history_count, + } + }; + + Ok(info) +} + fn main() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![ @@ -54,6 +114,7 @@ fn main() { search, global_stats, aliases, + home_info, dotfiles::aliases::import_aliases, dotfiles::aliases::delete_alias, dotfiles::aliases::set_alias, diff --git a/ui/package.json b/ui/package.json index 126adabe..e3025717 100644 --- a/ui/package.json +++ b/ui/package.json @@ -20,6 +20,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "core": "link:@tauri-apps/api/core", + "date-fns": "^3.6.0", "highlight.js": "^11.9.0", "lucide-react": "^0.367.0", "luxon": "^3.4.4", @@ -29,7 +30,8 @@ "recharts": "^2.12.4", "tailwind-merge": "^2.2.2", "tailwindcss-animate": "^1.0.7", - "vaul": "^0.9.0" + "vaul": "^0.9.0", + "zustand": "^4.5.2" }, "devDependencies": { "@tauri-apps/cli": "2.0.0-beta.2", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index dc202b20..1b4214fe 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -35,6 +35,9 @@ dependencies: core: specifier: link:@tauri-apps/api/core version: link:@tauri-apps/api/core + date-fns: + specifier: ^3.6.0 + version: 3.6.0 highlight.js: specifier: ^11.9.0 version: 11.9.0 @@ -65,6 +68,9 @@ dependencies: vaul: specifier: ^0.9.0 version: 0.9.0(@types/react-dom@18.2.24)(@types/react@18.2.74)(react-dom@18.2.0)(react@18.2.0) + zustand: + specifier: ^4.5.2 + version: 4.5.2(@types/react@18.2.74)(react@18.2.0) devDependencies: '@tauri-apps/cli': @@ -1786,6 +1792,10 @@ packages: engines: {node: '>=12'} dev: false + /date-fns@3.6.0: + resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + dev: false + /debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -2679,6 +2689,14 @@ packages: tslib: 2.6.2 dev: false + /use-sync-external-store@1.2.0(react@18.2.0): + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -2798,3 +2816,23 @@ packages: resolution: {integrity: sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==} engines: {node: '>= 14'} hasBin: true + + /zustand@4.5.2(@types/react@18.2.74)(react@18.2.0): + resolution: {integrity: sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + dependencies: + '@types/react': 18.2.74 + react: 18.2.0 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false 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> |