summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEllie Huxtable <ellie@elliehuxtable.com>2024-04-17 14:06:05 +0100
committerGitHub <noreply@github.com>2024-04-17 14:06:05 +0100
commitcb19925011d889c513e1bbedc446e399597e38a0 (patch)
tree7ad9e42013e15957805f2cdf563ce8b3e0c770f5
parentfcc0dc1bd576cd7b608e191c3a71e863d14b2d1f (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.rs14
-rw-r--r--atuin-client/src/history.rs2
-rw-r--r--atuin-client/src/record/sqlite_store.rs10
-rw-r--r--atuin-client/src/record/store.rs1
-rw-r--r--ui/backend/Cargo.lock6
-rw-r--r--ui/backend/src/main.rs61
-rw-r--r--ui/package.json4
-rw-r--r--ui/pnpm-lock.yaml38
-rw-r--r--ui/src/App.css8
-rw-r--r--ui/src/App.tsx18
-rw-r--r--ui/src/components/Drawer.tsx2
-rw-r--r--ui/src/components/HistoryList.tsx131
-rw-r--r--ui/src/components/dotfiles/Aliases.tsx37
-rw-r--r--ui/src/components/history/Stats.tsx56
-rw-r--r--ui/src/pages/Dotfiles.tsx7
-rw-r--r--ui/src/pages/History.tsx45
-rw-r--r--ui/src/pages/Home.tsx84
-rw-r--r--ui/src/state/models.ts34
-rw-r--r--ui/src/state/store.ts72
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>&nbsp;on&nbsp;</span>
+ <span>&nbsp;on&nbsp;</span>
- <span className="relative truncate ">
- {h.host}
- </span>
+ <span className="relative truncate ">{h.host}</span>
- <span>&nbsp;in&nbsp;</span>
+ <span>&nbsp;in&nbsp;</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>