mirror of
https://github.com/atuinsh/atuin.git
synced 2025-06-26 21:01:46 +02:00
feat(gui): use fancy new side nav (#2243)
* feat(gui): use fancy new side nav * compact only sidebar, no expand-collapse * custom drag region, remove titlebar * add user popup * wire up login/logout/register, move user button to bottom and add menu * link help and feedback to forum
This commit is contained in:
parent
353981e794
commit
8d9f677c4e
@ -14,6 +14,7 @@ pub mod history;
|
|||||||
pub mod import;
|
pub mod import;
|
||||||
pub mod kv;
|
pub mod kv;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
|
pub mod logout;
|
||||||
pub mod ordering;
|
pub mod ordering;
|
||||||
pub mod record;
|
pub mod record;
|
||||||
pub mod register;
|
pub mod register;
|
||||||
|
17
crates/atuin-client/src/logout.rs
Normal file
17
crates/atuin-client/src/logout.rs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
use eyre::{Context, Result};
|
||||||
|
use fs_err::remove_file;
|
||||||
|
|
||||||
|
use crate::settings::Settings;
|
||||||
|
|
||||||
|
pub fn logout(settings: &Settings) -> Result<()> {
|
||||||
|
let session_path = settings.session_path.as_str();
|
||||||
|
|
||||||
|
if settings.logged_in() {
|
||||||
|
remove_file(session_path).context("Failed to remove session file")?;
|
||||||
|
println!("You have logged out!");
|
||||||
|
} else {
|
||||||
|
println!("You are not logged in");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -1,17 +1,6 @@
|
|||||||
use eyre::{Context, Result};
|
|
||||||
use fs_err::remove_file;
|
|
||||||
|
|
||||||
use atuin_client::settings::Settings;
|
use atuin_client::settings::Settings;
|
||||||
|
use eyre::Result;
|
||||||
|
|
||||||
pub fn run(settings: &Settings) -> Result<()> {
|
pub fn run(settings: &Settings) -> Result<()> {
|
||||||
let session_path = settings.session_path.as_str();
|
atuin_client::logout::logout(settings)
|
||||||
|
|
||||||
if settings.logged_in() {
|
|
||||||
remove_file(session_path).context("Failed to remove session file")?;
|
|
||||||
println!("You have logged out!");
|
|
||||||
} else {
|
|
||||||
println!("You are not logged in");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
1
ui/backend/Cargo.lock
generated
1
ui/backend/Cargo.lock
generated
@ -6481,6 +6481,7 @@ dependencies = [
|
|||||||
"atuin-dotfiles",
|
"atuin-dotfiles",
|
||||||
"atuin-history",
|
"atuin-history",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"cocoa",
|
||||||
"comrak",
|
"comrak",
|
||||||
"eyre",
|
"eyre",
|
||||||
"nix 0.29.0",
|
"nix 0.29.0",
|
||||||
|
@ -18,7 +18,7 @@ atuin-dotfiles = { path = "../../crates/atuin-dotfiles", version = "0.4.0" }
|
|||||||
atuin-history = { path = "../../crates/atuin-history", version = "0.3.0" }
|
atuin-history = { path = "../../crates/atuin-history", version = "0.3.0" }
|
||||||
|
|
||||||
eyre = "0.6"
|
eyre = "0.6"
|
||||||
tauri = { version = "2.0.0-beta", features = ["tray-icon"] }
|
tauri = { version = "2.0.0-beta", features = [ "tray-icon"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
time = "0.3.36"
|
time = "0.3.36"
|
||||||
@ -29,11 +29,14 @@ tauri-plugin-single-instance = "2.0.0-beta"
|
|||||||
tokio = "1.38.0"
|
tokio = "1.38.0"
|
||||||
tauri-plugin-shell = "2.0.0-beta.7"
|
tauri-plugin-shell = "2.0.0-beta.7"
|
||||||
comrak = "0.22"
|
comrak = "0.22"
|
||||||
portable-pty = "0.8.1"
|
portable-pty = "0.8.1"
|
||||||
vt100 = "0.15.2"
|
vt100 = "0.15.2"
|
||||||
bytes = "1.6.0"
|
bytes = "1.6.0"
|
||||||
nix = "0.29.0"
|
nix = "0.29.0"
|
||||||
|
|
||||||
|
[target."cfg(target_os = \"macos\")".dependencies]
|
||||||
|
cocoa = "0.25"
|
||||||
|
|
||||||
[dependencies.sqlx]
|
[dependencies.sqlx]
|
||||||
version = "0.7"
|
version = "0.7"
|
||||||
features = ["runtime-tokio-rustls", "time", "postgres", "uuid"]
|
features = ["runtime-tokio-rustls", "time", "postgres", "uuid"]
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
"sql:allow-load",
|
"sql:allow-load",
|
||||||
"sql:allow-execute",
|
"sql:allow-execute",
|
||||||
"sql:allow-select",
|
"sql:allow-select",
|
||||||
|
"window:allow-start-dragging",
|
||||||
{
|
{
|
||||||
"identifier": "http:default",
|
"identifier": "http:default",
|
||||||
"allow": ["https://api.atuin.sh/*"]
|
"allow": ["https://api.atuin.sh/*"]
|
||||||
|
@ -110,6 +110,16 @@ async fn login(username: String, password: String, key: String) -> Result<String
|
|||||||
Ok(session)
|
Ok(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn logout() -> Result<(), String> {
|
||||||
|
let settings = Settings::new().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
atuin_client::logout::logout(&settings)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn register(username: String, email: String, password: String) -> Result<String, String> {
|
async fn register(username: String, email: String, password: String) -> Result<String, String> {
|
||||||
let settings = Settings::new().map_err(|e| e.to_string())?;
|
let settings = Settings::new().map_err(|e| e.to_string())?;
|
||||||
@ -257,6 +267,7 @@ fn main() {
|
|||||||
config,
|
config,
|
||||||
session,
|
session,
|
||||||
login,
|
login,
|
||||||
|
logout,
|
||||||
register,
|
register,
|
||||||
history_calendar,
|
history_calendar,
|
||||||
run::pty::pty_open,
|
run::pty::pty_open,
|
||||||
|
@ -15,7 +15,9 @@
|
|||||||
"width": 1200,
|
"width": 1200,
|
||||||
"height": 800,
|
"height": 800,
|
||||||
"minWidth": 1000,
|
"minWidth": 1000,
|
||||||
"minHeight": 500
|
"minHeight": 500,
|
||||||
|
"titleBarStyle": "Overlay",
|
||||||
|
"hiddenTitle": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
"@codemirror/view": "^6.28.2",
|
"@codemirror/view": "^6.28.2",
|
||||||
"@headlessui/react": "^2.1.1",
|
"@headlessui/react": "^2.1.1",
|
||||||
"@heroicons/react": "^2.1.4",
|
"@heroicons/react": "^2.1.4",
|
||||||
|
"@nextui-org/react": "^2.4.2",
|
||||||
"@radix-ui/react-dialog": "^1.1.1",
|
"@radix-ui/react-dialog": "^1.1.1",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
@ -42,6 +43,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"core": "link:@tauri-apps/api/core",
|
"core": "link:@tauri-apps/api/core",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
|
"framer-motion": "^11.2.13",
|
||||||
"highlight.js": "^11.9.0",
|
"highlight.js": "^11.9.0",
|
||||||
"lucide-react": "^0.402.0",
|
"lucide-react": "^0.402.0",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
@ -51,16 +53,19 @@
|
|||||||
"react-activity-calendar": "^2.2.10",
|
"react-activity-calendar": "^2.2.10",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-spinners": "^0.14.1",
|
"react-spinners": "^0.14.1",
|
||||||
|
"react-router-dom": "^6.24.1",
|
||||||
"react-tooltip": "^5.27.0",
|
"react-tooltip": "^5.27.0",
|
||||||
"react-window": "^1.8.10",
|
"react-window": "^1.8.10",
|
||||||
"react-window-infinite-loader": "^1.0.9",
|
"react-window-infinite-loader": "^1.0.9",
|
||||||
"recharts": "^2.12.7",
|
"recharts": "^2.12.7",
|
||||||
"tailwind-merge": "^2.3.0",
|
"tailwind-merge": "^2.3.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"usehooks-ts": "^3.1.0",
|
||||||
"vaul": "^0.9.1",
|
"vaul": "^0.9.1",
|
||||||
"zustand": "^4.5.2"
|
"zustand": "^4.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@iconify/react": "^5.0.1",
|
||||||
"@tauri-apps/cli": "2.0.0-beta.20",
|
"@tauri-apps/cli": "2.0.0-beta.20",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
|
9729
ui/pnpm-lock.yaml
generated
9729
ui/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
257
ui/src/App.tsx
257
ui/src/App.tsx
@ -1,10 +1,17 @@
|
|||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
import { open } from "@tauri-apps/plugin-shell";
|
||||||
|
|
||||||
import { useState, ReactElement } from "react";
|
import { useState, ReactElement, useEffect } from "react";
|
||||||
import { useStore } from "@/state/store";
|
import { useStore } from "@/state/store";
|
||||||
|
|
||||||
import Button, { ButtonStyle } from "@/components/Button";
|
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
|
import {
|
||||||
|
SettingsIcon,
|
||||||
|
CircleHelpIcon,
|
||||||
|
KeyRoundIcon,
|
||||||
|
LogOutIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Icon } from "@iconify/react";
|
||||||
|
|
||||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||||
|
|
||||||
@ -28,6 +35,35 @@ import Dotfiles from "./pages/Dotfiles.tsx";
|
|||||||
import LoginOrRegister from "./components/LoginOrRegister.tsx";
|
import LoginOrRegister from "./components/LoginOrRegister.tsx";
|
||||||
import Runbooks from "./pages/Runbooks.tsx";
|
import Runbooks from "./pages/Runbooks.tsx";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
User,
|
||||||
|
Button,
|
||||||
|
ScrollShadow,
|
||||||
|
Spacer,
|
||||||
|
Tooltip,
|
||||||
|
Dropdown,
|
||||||
|
DropdownItem,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownSection,
|
||||||
|
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 {
|
enum Section {
|
||||||
Home,
|
Home,
|
||||||
History,
|
History,
|
||||||
@ -54,90 +90,165 @@ function App() {
|
|||||||
// pages
|
// pages
|
||||||
const [section, setSection] = useState(Section.Home);
|
const [section, setSection] = useState(Section.Home);
|
||||||
const user = useStore((state) => state.user);
|
const user = useStore((state) => state.user);
|
||||||
console.log(user);
|
const refreshUser = useStore((state) => state.refreshUser);
|
||||||
|
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||||
|
|
||||||
const navigation = [
|
const navigation: SidebarItem[] = [
|
||||||
{
|
{
|
||||||
name: "Home",
|
key: "personal",
|
||||||
icon: HomeIcon,
|
title: "Personal",
|
||||||
section: Section.Home,
|
items: [
|
||||||
},
|
{
|
||||||
{
|
key: "home",
|
||||||
name: "History",
|
icon: "solar:home-2-linear",
|
||||||
icon: ClockIcon,
|
title: "Home",
|
||||||
section: Section.History,
|
onPress: () => setSection(Section.Home),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Dotfiles",
|
key: "runbooks",
|
||||||
icon: WrenchScrewdriverIcon,
|
icon: "solar:notebook-linear",
|
||||||
section: Section.Dotfiles,
|
title: "Runbooks",
|
||||||
},
|
onPress: () => {
|
||||||
{
|
console.log("runbooks");
|
||||||
name: "Runbooks",
|
setSection(Section.Runbooks);
|
||||||
icon: ChevronRightSquare,
|
},
|
||||||
section: Section.Runbooks,
|
},
|
||||||
|
{
|
||||||
|
key: "history",
|
||||||
|
icon: "solar:history-outline",
|
||||||
|
title: "History",
|
||||||
|
onPress: () => setSection(Section.History),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "dotfiles",
|
||||||
|
icon: "solar:file-smile-linear",
|
||||||
|
title: "Dotfiles",
|
||||||
|
onPress: () => setSection(Section.Dotfiles),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex h-dvh w-full select-none">
|
||||||
<div className="fixed inset-y-0 z-50 flex w-60 flex-col">
|
<div className="relative flex h-full flex-col !border-r-small border-divider p-6 transition-width px-2 pb-6 pt-9 w-16 items-center">
|
||||||
<div className="flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200 bg-white px-6 pb-4">
|
<div className="flex items-center gap-0 px-3 justify-center">
|
||||||
<div className="flex h-16 shrink-0 items-center">
|
<div className="flex h-8 w-8">
|
||||||
<img className="h-8 w-auto" src={Logo} alt="Atuin" />
|
<img src={icon} alt="icon" className="h-8 w-8" />
|
||||||
</div>
|
</div>
|
||||||
<nav className="flex flex-1 flex-col">
|
</div>
|
||||||
<ul role="list" className="flex flex-1 flex-col gap-y-7">
|
|
||||||
<li>
|
<ScrollShadow className="-mr-6 h-full max-h-full py-6 pr-6">
|
||||||
<ul role="list" className="-mx-2 space-y-1 w-full">
|
<Sidebar
|
||||||
{navigation.map((item) => (
|
defaultSelectedKey="home"
|
||||||
<li key={item.name}>
|
isCompact={true}
|
||||||
<button
|
items={navigation}
|
||||||
onClick={() => setSection(item.section)}
|
/>
|
||||||
className={classNames(
|
</ScrollShadow>
|
||||||
section == item.section
|
|
||||||
? "bg-gray-50 text-green-600"
|
<Spacer y={2} />
|
||||||
: "text-gray-700 hover:text-green-600 hover:bg-gray-50",
|
|
||||||
"group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold w-full",
|
<div className="flex items-center gap-3 px-3">
|
||||||
)}
|
<Dropdown showArrow placement="right-start">
|
||||||
>
|
<DropdownTrigger>
|
||||||
<item.icon
|
<Button disableRipple isIconOnly radius="full" variant="light">
|
||||||
className={classNames(
|
<Avatar
|
||||||
section == item.section
|
isBordered
|
||||||
? "text-green-600"
|
className="flex-none"
|
||||||
: "text-gray-400 group-hover:text-green-600",
|
size="sm"
|
||||||
"h-6 w-6 shrink-0",
|
name={user.username || ""}
|
||||||
)}
|
/>
|
||||||
aria-hidden="true"
|
</Button>
|
||||||
/>
|
</DropdownTrigger>
|
||||||
{item.name}
|
<DropdownMenu aria-label="Custom item styles">
|
||||||
</button>
|
<DropdownItem
|
||||||
</li>
|
key="profile"
|
||||||
))}
|
isReadOnly
|
||||||
</ul>
|
className="h-14 opacity-100"
|
||||||
</li>
|
textValue="Signed in as"
|
||||||
<li className="mt-auto">
|
>
|
||||||
{user && user.username === "" && !user.username && (
|
<User
|
||||||
<Dialog>
|
avatarProps={{
|
||||||
<DialogTrigger className="w-full">
|
size: "sm",
|
||||||
<Button
|
name: user.username || "Anonymous User",
|
||||||
text={"Login or Register"}
|
showFallback: true,
|
||||||
style={ButtonStyle.PrimarySmFill}
|
imgProps: {
|
||||||
/>
|
className: "transition-none",
|
||||||
</DialogTrigger>
|
},
|
||||||
<DialogContent>
|
}}
|
||||||
<LoginOrRegister />
|
classNames={{
|
||||||
</DialogContent>
|
name: "text-default-600",
|
||||||
</Dialog>
|
description: "text-default-500",
|
||||||
|
}}
|
||||||
|
description={
|
||||||
|
user.bio || (user.username && "No bio") || "Sign up now"
|
||||||
|
}
|
||||||
|
name={user.username || "Anonymous User"}
|
||||||
|
/>
|
||||||
|
</DropdownItem>
|
||||||
|
|
||||||
|
<DropdownItem
|
||||||
|
key="settings"
|
||||||
|
description="Configure Atuin"
|
||||||
|
startContent={<Icon icon="solar:settings-linear" width={24} />}
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</DropdownItem>
|
||||||
|
|
||||||
|
<DropdownSection aria-label="Help & Feedback">
|
||||||
|
<DropdownItem
|
||||||
|
key="help_and_feedback"
|
||||||
|
description="Get in touch"
|
||||||
|
onPress={() => open("https://forum.atuin.sh")}
|
||||||
|
startContent={
|
||||||
|
<Icon width={24} icon="solar:question-circle-linear" />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Help & Feedback
|
||||||
|
</DropdownItem>
|
||||||
|
|
||||||
|
{(user.username && (
|
||||||
|
<DropdownItem
|
||||||
|
key="logout"
|
||||||
|
startContent={
|
||||||
|
<Icon width={24} icon="solar:logout-broken" />
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
logout();
|
||||||
|
refreshUser();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Log Out
|
||||||
|
</DropdownItem>
|
||||||
|
)) || (
|
||||||
|
<DropdownItem
|
||||||
|
key="signup"
|
||||||
|
description="Sync, backup and share your data"
|
||||||
|
className="bg-emerald-100"
|
||||||
|
startContent={<KeyRoundIcon size="18px" />}
|
||||||
|
onPress={onOpen}
|
||||||
|
>
|
||||||
|
Log in or Register
|
||||||
|
</DropdownItem>
|
||||||
)}
|
)}
|
||||||
</li>
|
</DropdownSection>
|
||||||
</ul>
|
</DropdownMenu>
|
||||||
</nav>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{renderMain(section)}
|
{renderMain(section)}
|
||||||
|
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange} placement="top-center">
|
||||||
|
<ModalContent className="p-8">
|
||||||
|
{(onClose) => (
|
||||||
|
<>
|
||||||
|
<LoginOrRegister onClose={onClose} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
1
ui/src/assets/icon.svg
Normal file
1
ui/src/assets/icon.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 12 KiB |
@ -6,6 +6,7 @@ import { useStore } from "@/state/store";
|
|||||||
|
|
||||||
interface LoginProps {
|
interface LoginProps {
|
||||||
toggleRegister: () => void;
|
toggleRegister: () => void;
|
||||||
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Login(props: LoginProps) {
|
function Login(props: LoginProps) {
|
||||||
@ -24,7 +25,7 @@ function Login(props: LoginProps) {
|
|||||||
try {
|
try {
|
||||||
await login(username, password, key);
|
await login(username, password, key);
|
||||||
refreshUser();
|
refreshUser();
|
||||||
console.log("Logged in");
|
props.onClose();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setErrors(e);
|
setErrors(e);
|
||||||
@ -171,6 +172,7 @@ function Login(props: LoginProps) {
|
|||||||
|
|
||||||
interface RegisterProps {
|
interface RegisterProps {
|
||||||
toggleLogin: () => void;
|
toggleLogin: () => void;
|
||||||
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Register(props: RegisterProps) {
|
function Register(props: RegisterProps) {
|
||||||
@ -185,13 +187,11 @@ function Register(props: RegisterProps) {
|
|||||||
const email = form.email.value;
|
const email = form.email.value;
|
||||||
const password = form.password.value;
|
const password = form.password.value;
|
||||||
|
|
||||||
console.log("Logging in...");
|
|
||||||
try {
|
try {
|
||||||
await register(username, email, password);
|
await register(username, email, password);
|
||||||
refreshUser();
|
refreshUser();
|
||||||
console.log("Logged in");
|
props.onClose();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
|
||||||
setErrors(e);
|
setErrors(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -330,12 +330,12 @@ function Register(props: RegisterProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LoginOrRegister() {
|
export default function LoginOrRegister({ onClose }: { onClose: () => void }) {
|
||||||
let [login, setLogin] = useState<boolean>(false);
|
let [login, setLogin] = useState<boolean>(false);
|
||||||
|
|
||||||
if (login) {
|
if (login) {
|
||||||
return <Login toggleRegister={() => setLogin(false)} />;
|
return <Login onClose={onClose} toggleRegister={() => setLogin(false)} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Register toggleLogin={() => setLogin(true)} />;
|
return <Register onClose={onClose} toggleLogin={() => setLogin(true)} />;
|
||||||
}
|
}
|
||||||
|
328
ui/src/components/Sidebar/Sidebar.tsx
Normal file
328
ui/src/components/Sidebar/Sidebar.tsx
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionItem,
|
||||||
|
type ListboxProps,
|
||||||
|
type ListboxSectionProps,
|
||||||
|
type Selection,
|
||||||
|
} from "@nextui-org/react";
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Listbox,
|
||||||
|
Tooltip,
|
||||||
|
ListboxItem,
|
||||||
|
ListboxSection,
|
||||||
|
} from "@nextui-org/react";
|
||||||
|
import { Icon } from "@iconify/react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export enum SidebarItemType {
|
||||||
|
Nest = "nest",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SidebarItem = {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
icon?: string;
|
||||||
|
href?: string;
|
||||||
|
onPress?: () => void;
|
||||||
|
type?: SidebarItemType.Nest;
|
||||||
|
startContent?: React.ReactNode;
|
||||||
|
endContent?: React.ReactNode;
|
||||||
|
items?: SidebarItem[];
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SidebarProps = Omit<ListboxProps<SidebarItem>, "children"> & {
|
||||||
|
items: SidebarItem[];
|
||||||
|
isCompact?: boolean;
|
||||||
|
hideEndContent?: boolean;
|
||||||
|
iconClassName?: string;
|
||||||
|
sectionClasses?: ListboxSectionProps["classNames"];
|
||||||
|
classNames?: ListboxProps["classNames"];
|
||||||
|
defaultSelectedKey: string;
|
||||||
|
onSelect?: (key: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Sidebar = React.forwardRef<HTMLElement, SidebarProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
items,
|
||||||
|
isCompact,
|
||||||
|
defaultSelectedKey,
|
||||||
|
onSelect,
|
||||||
|
hideEndContent,
|
||||||
|
sectionClasses: sectionClassesProp = {},
|
||||||
|
itemClasses: itemClassesProp = {},
|
||||||
|
iconClassName,
|
||||||
|
classNames,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const [selected, setSelected] =
|
||||||
|
React.useState<React.Key>(defaultSelectedKey);
|
||||||
|
|
||||||
|
const sectionClasses = {
|
||||||
|
...sectionClassesProp,
|
||||||
|
base: cn(sectionClassesProp?.base, "w-full", {
|
||||||
|
"p-0 max-w-[44px]": isCompact,
|
||||||
|
}),
|
||||||
|
group: cn(sectionClassesProp?.group, {
|
||||||
|
"flex flex-col gap-1": isCompact,
|
||||||
|
}),
|
||||||
|
heading: cn(sectionClassesProp?.heading, {
|
||||||
|
hidden: isCompact,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemClasses = {
|
||||||
|
...itemClassesProp,
|
||||||
|
base: cn(itemClassesProp?.base, {
|
||||||
|
"w-11 h-11 gap-0 p-0": isCompact,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderNestItem = React.useCallback(
|
||||||
|
(item: SidebarItem) => {
|
||||||
|
const isNestType =
|
||||||
|
item.items &&
|
||||||
|
item.items?.length > 0 &&
|
||||||
|
item?.type === SidebarItemType.Nest;
|
||||||
|
|
||||||
|
if (isNestType) {
|
||||||
|
// Is a nest type item , so we need to remove the href
|
||||||
|
delete item.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListboxItem
|
||||||
|
{...item}
|
||||||
|
key={item.key}
|
||||||
|
classNames={{
|
||||||
|
base: cn(
|
||||||
|
{
|
||||||
|
"h-auto p-0": !isCompact && isNestType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inline-block w-11": isCompact && isNestType,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
endContent={
|
||||||
|
isCompact || isNestType || hideEndContent
|
||||||
|
? null
|
||||||
|
: item.endContent ?? null
|
||||||
|
}
|
||||||
|
startContent={
|
||||||
|
isCompact || isNestType ? null : item.icon ? (
|
||||||
|
<Icon
|
||||||
|
className={cn(
|
||||||
|
"text-default-500 group-data-[selected=true]:text-foreground",
|
||||||
|
iconClassName,
|
||||||
|
)}
|
||||||
|
icon={item.icon}
|
||||||
|
width={24}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
item.startContent ?? null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
title={isCompact || isNestType ? null : item.title}
|
||||||
|
>
|
||||||
|
{isCompact ? (
|
||||||
|
<Tooltip content={item.title} placement="right">
|
||||||
|
<div className="flex w-full items-center justify-center">
|
||||||
|
{item.icon ? (
|
||||||
|
<Icon
|
||||||
|
className={cn(
|
||||||
|
"text-default-500 group-data-[selected=true]:text-foreground",
|
||||||
|
iconClassName,
|
||||||
|
)}
|
||||||
|
icon={item.icon}
|
||||||
|
width={24}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
item.startContent ?? null
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
|
{!isCompact && isNestType ? (
|
||||||
|
<Accordion className={"p-0"}>
|
||||||
|
<AccordionItem
|
||||||
|
key={item.key}
|
||||||
|
aria-label={item.title}
|
||||||
|
classNames={{
|
||||||
|
heading: "pr-3",
|
||||||
|
trigger: "p-0",
|
||||||
|
content: "py-0 pl-4",
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
item.icon ? (
|
||||||
|
<div
|
||||||
|
className={"flex h-11 items-center gap-2 px-2 py-1.5"}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
className={cn(
|
||||||
|
"text-default-500 group-data-[selected=true]:text-foreground",
|
||||||
|
iconClassName,
|
||||||
|
)}
|
||||||
|
icon={item.icon}
|
||||||
|
width={24}
|
||||||
|
/>
|
||||||
|
<span className="text-small font-medium text-default-500 group-data-[selected=true]:text-foreground">
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
item.startContent ?? null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.items && item.items?.length > 0 ? (
|
||||||
|
<Listbox
|
||||||
|
className={"mt-0.5"}
|
||||||
|
classNames={{
|
||||||
|
list: cn("border-l border-default-200 pl-4"),
|
||||||
|
}}
|
||||||
|
items={item.items}
|
||||||
|
variant="flat"
|
||||||
|
>
|
||||||
|
{item.items.map(renderItem)}
|
||||||
|
</Listbox>
|
||||||
|
) : (
|
||||||
|
renderItem(item)
|
||||||
|
)}
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
) : null}
|
||||||
|
</ListboxItem>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[isCompact, hideEndContent, iconClassName, items],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderItem = React.useCallback(
|
||||||
|
(item: SidebarItem) => {
|
||||||
|
const isNestType =
|
||||||
|
item.items &&
|
||||||
|
item.items?.length > 0 &&
|
||||||
|
item?.type === SidebarItemType.Nest;
|
||||||
|
|
||||||
|
if (isNestType) {
|
||||||
|
return renderNestItem(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListboxItem
|
||||||
|
{...item}
|
||||||
|
key={item.key}
|
||||||
|
endContent={
|
||||||
|
isCompact || hideEndContent ? null : item.endContent ?? null
|
||||||
|
}
|
||||||
|
startContent={
|
||||||
|
isCompact ? null : item.icon ? (
|
||||||
|
<Icon
|
||||||
|
className={cn(
|
||||||
|
"text-default-500 group-data-[selected=true]:text-foreground",
|
||||||
|
iconClassName,
|
||||||
|
)}
|
||||||
|
icon={item.icon}
|
||||||
|
width={24}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
item.startContent ?? null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
textValue={item.title}
|
||||||
|
title={isCompact ? null : item.title}
|
||||||
|
>
|
||||||
|
{isCompact ? (
|
||||||
|
<Tooltip content={item.title} placement="right">
|
||||||
|
<div className="flex w-full items-center justify-center">
|
||||||
|
{item.icon ? (
|
||||||
|
<Icon
|
||||||
|
className={cn(
|
||||||
|
"text-default-500 group-data-[selected=true]:text-foreground",
|
||||||
|
iconClassName,
|
||||||
|
)}
|
||||||
|
icon={item.icon}
|
||||||
|
width={24}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
item.startContent ?? null
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
|
</ListboxItem>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[isCompact, hideEndContent, iconClassName, itemClasses?.base],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Listbox
|
||||||
|
key={isCompact ? "compact" : "default"}
|
||||||
|
ref={ref}
|
||||||
|
hideSelectedIcon
|
||||||
|
as="nav"
|
||||||
|
className={cn("list-none", className)}
|
||||||
|
classNames={{
|
||||||
|
...classNames,
|
||||||
|
list: cn("items-center", classNames?.list),
|
||||||
|
}}
|
||||||
|
color="default"
|
||||||
|
itemClasses={{
|
||||||
|
...itemClasses,
|
||||||
|
base: cn(
|
||||||
|
"px-3 min-h-11 rounded-large h-[44px] data-[selected=true]:bg-default-100",
|
||||||
|
itemClasses?.base,
|
||||||
|
),
|
||||||
|
title: cn(
|
||||||
|
"text-small font-medium text-default-500 group-data-[selected=true]:text-foreground",
|
||||||
|
itemClasses?.title,
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
items={items}
|
||||||
|
selectedKeys={[selected] as unknown as Selection}
|
||||||
|
selectionMode="single"
|
||||||
|
variant="flat"
|
||||||
|
onSelectionChange={(keys) => {
|
||||||
|
const key = Array.from(keys)[0];
|
||||||
|
|
||||||
|
setSelected(key as React.Key);
|
||||||
|
onSelect?.(key as string);
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{(item) => {
|
||||||
|
return item.items &&
|
||||||
|
item.items?.length > 0 &&
|
||||||
|
item?.type === SidebarItemType.Nest ? (
|
||||||
|
renderNestItem(item)
|
||||||
|
) : item.items && item.items?.length > 0 ? (
|
||||||
|
<ListboxSection
|
||||||
|
key={item.key}
|
||||||
|
classNames={sectionClasses}
|
||||||
|
showDivider={isCompact}
|
||||||
|
title={item.title}
|
||||||
|
>
|
||||||
|
{item.items.map(renderItem)}
|
||||||
|
</ListboxSection>
|
||||||
|
) : (
|
||||||
|
renderItem(item)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Listbox>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Sidebar.displayName = "Sidebar";
|
||||||
|
|
||||||
|
export default Sidebar;
|
4
ui/src/components/Sidebar/index.tsx
Normal file
4
ui/src/components/Sidebar/index.tsx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import Sidebar, { SidebarItem } from "./Sidebar";
|
||||||
|
|
||||||
|
export type { SidebarItem };
|
||||||
|
export default Sidebar;
|
@ -60,6 +60,10 @@ export default function Editor() {
|
|||||||
content: "Atuin runbooks",
|
content: "Atuin runbooks",
|
||||||
id: "foo",
|
id: "foo",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: "run",
|
||||||
|
id: "bar",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,6 +1,31 @@
|
|||||||
import { type ClassValue, clsx } from "clsx"
|
import type { ClassValue } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge"
|
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { extendTailwindMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
const COMMON_UNITS = ["small", "medium", "large"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We need to extend the tailwind merge to include NextUI's custom classes.
|
||||||
|
*
|
||||||
|
* So we can use classes like `text-small` or `text-default-500` and override them.
|
||||||
|
*/
|
||||||
|
const twMerge = extendTailwindMerge({
|
||||||
|
extend: {
|
||||||
|
theme: {
|
||||||
|
opacity: ["disabled"],
|
||||||
|
spacing: ["divider"],
|
||||||
|
borderWidth: COMMON_UNITS,
|
||||||
|
borderRadius: COMMON_UNITS,
|
||||||
|
},
|
||||||
|
classGroups: {
|
||||||
|
shadow: [{ shadow: COMMON_UNITS }],
|
||||||
|
"font-size": [{ text: ["tiny", ...COMMON_UNITS] }],
|
||||||
|
"bg-image": ["bg-stripe-gradient"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
|
import { NextUIProvider, Spacer } from "@nextui-org/react";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import "./styles.css";
|
import "./styles.css";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<NextUIProvider>
|
||||||
|
<div data-tauri-drag-region className="w-full h-8 absolute" />
|
||||||
|
<App />
|
||||||
|
</NextUIProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
@ -98,7 +98,7 @@ export default function Dotfiles() {
|
|||||||
console.log(current);
|
console.log(current);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pl-60">
|
<div className="w-full flex-1 flex-col p-4">
|
||||||
<div className="p-10">
|
<div className="p-10">
|
||||||
<Header current={current} setCurrent={setCurrent} />
|
<Header current={current} setCurrent={setCurrent} />
|
||||||
Manage your shell aliases, variables and paths
|
Manage your shell aliases, variables and paths
|
||||||
|
@ -84,7 +84,7 @@ export default function Search() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="pl-60">
|
<div className="w-full flex-1 flex-col p-4">
|
||||||
<div className="p-10 history-header">
|
<div className="p-10 history-header">
|
||||||
<Header />
|
<Header />
|
||||||
<p>A history of all the commands you run in your shell.</p>
|
<p>A history of all the commands you run in your shell.</p>
|
||||||
|
@ -125,7 +125,7 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pl-60">
|
<div className="w-full flex-1 flex-col p-4">
|
||||||
<div className="p-10">
|
<div className="p-10">
|
||||||
<Header name={user.username} />
|
<Header name={user.username} />
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import Editor from "@/components/runbooks/editor/Editor";
|
|||||||
|
|
||||||
export default function Runbooks() {
|
export default function Runbooks() {
|
||||||
return (
|
return (
|
||||||
<div className="pl-60 p-4 ">
|
<div className="w-full flex-1 flex-col p-4">
|
||||||
<Editor />
|
<Editor />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -20,6 +20,10 @@ export async function login(
|
|||||||
return await invoke("login", { username, password, key });
|
return await invoke("login", { username, password, key });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function logout(): Promise<string> {
|
||||||
|
return await invoke("logout");
|
||||||
|
}
|
||||||
|
|
||||||
export async function register(
|
export async function register(
|
||||||
username: string,
|
username: string,
|
||||||
email: string,
|
email: string,
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
|
const { nextui } = require("@nextui-org/react");
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
darkMode: ["class"],
|
darkMode: "class",
|
||||||
content: [
|
content: [
|
||||||
'./pages/**/*.{ts,tsx}',
|
"./pages/**/*.{ts,tsx}",
|
||||||
'./components/**/*.{ts,tsx}',
|
"./components/**/*.{ts,tsx}",
|
||||||
'./app/**/*.{ts,tsx}',
|
"./app/**/*.{ts,tsx}",
|
||||||
'./src/**/*.{ts,tsx}',
|
"./src/**/*.{ts,tsx}",
|
||||||
|
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
|
||||||
],
|
],
|
||||||
prefix: "",
|
prefix: "",
|
||||||
theme: {
|
theme: {
|
||||||
@ -73,5 +76,5 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [require("tailwindcss-animate")],
|
plugins: [require("tailwindcss-animate"), nextui()],
|
||||||
}
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user