From f8c963c7d668fc57680f25413f20bc207d4bf64a Mon Sep 17 00:00:00 2001
From: Ellie Huxtable <ellie@atuin.sh>
Date: Tue, 23 Jul 2024 13:18:54 +0100
Subject: feat(gui): clean up home page, fix a few bugs (#2304)

* wip home screen changes

* more

* adjust

* fixes and things

* patch runbook pty check
---
 ui/src/pages/Home.tsx | 255 +++++++++++++++++++++++++++++++++++++-------------
 1 file changed, 188 insertions(+), 67 deletions(-)

(limited to 'ui/src/pages/Home.tsx')

diff --git a/ui/src/pages/Home.tsx b/ui/src/pages/Home.tsx
index 0f0b5dcf..2e93a893 100644
--- a/ui/src/pages/Home.tsx
+++ b/ui/src/pages/Home.tsx
@@ -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"
-          >
-            <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>
+    <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,
+        }}
+      >
+        <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,49 +198,97 @@ 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>
+      <div className="p-10 grid grid-cols-4 gap-4">
+        <StatCard
+          name="Last Sync"
+          stat={
+            (homeInfo.lastSyncTime &&
+              formatRelative(homeInfo.lastSyncTime, new Date())) ||
+            "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 className="pt-10">
-          <Stats
-            stats={[
-              {
-                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,
-              },
-            ]}
-          />
-        </div>
-
-        <div className="pt-10 flex justify-around">
-          <ActivityCalendar
-            theme={explicitTheme}
-            data={calendar}
-            weekStart={weekStart as any}
-            renderBlock={(block, activity) =>
-              React.cloneElement(block, {
-                "data-tooltip-id": "react-tooltip",
-                "data-tooltip-html": `${activity.count} commands on ${activity.date}`,
-              })
-            }
-            labels={{
-              totalCount: "{{count}} history records in the last year",
-            }}
-          />
-          <ReactTooltip id="react-tooltip" />
-        </div>
+        <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}
+              renderBlock={(block, activity) =>
+                React.cloneElement(block, {
+                  "data-tooltip-id": "react-tooltip",
+                  "data-tooltip-html": `${activity.count} commands on ${activity.date}`,
+                })
+              }
+            />
+            <ReactTooltip id="react-tooltip" />
+          </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>
   );
-- 
cgit v1.3.1

