feat(ui/dotfiles): add vars (#1989)

This commit is contained in:
Ellie Huxtable 2024-04-29 14:59:59 +01:00 committed by GitHub
parent 150bcb8eb8
commit cea48a1545
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 364 additions and 10 deletions

8
ui/backend/Cargo.lock generated
View File

@ -5285,18 +5285,18 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "typed-builder"
version = "0.18.1"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "444d8748011b93cb168770e8092458cb0f8854f931ff82fdf6ddfbd72a9c933e"
checksum = "77739c880e00693faef3d65ea3aad725f196da38b22fdc7ea6ded6e1ce4d3add"
dependencies = [
"typed-builder-macro",
]
[[package]]
name = "typed-builder-macro"
version = "0.18.1"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "563b3b88238ec95680aef36bdece66896eaa7ce3c0f1b4f39d38fb2435261352"
checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63"
dependencies = [
"proc-macro2",
"quote",

View File

@ -12,10 +12,10 @@ edition = "2021"
tauri-build = { version = "2.0.0-beta", features = [] }
[dependencies]
atuin-client = { path = "../../atuin-client", version = "18.2.0" }
atuin-common = { path = "../../atuin-common", version = "18.2.0" }
atuin-client = { path = "../../crates/atuin-client", version = "18.2.0" }
atuin-common = { path = "../../crates/atuin-common", version = "18.2.0" }
atuin-dotfiles = { path = "../../atuin-dotfiles", version = "0.2.0" }
atuin-dotfiles = { path = "../../crates/atuin-dotfiles", version = "0.2.0" }
eyre = "0.6"
tauri = { version = "2.0.0-beta", features = ["tray-icon"] }

View File

@ -1 +1,2 @@
pub mod aliases;
pub mod vars;

View File

@ -0,0 +1,57 @@
use std::path::PathBuf;
use atuin_client::{encryption, record::sqlite_store::SqliteStore, settings::Settings};
use atuin_common::shell::Shell;
use atuin_dotfiles::{
shell::{existing_aliases, Alias, Var},
store::var::VarStore,
};
async fn var_store() -> eyre::Result<VarStore> {
let settings = Settings::new()?;
let record_store_path = PathBuf::from(settings.record_store_path.as_str());
let sqlite_store = SqliteStore::new(record_store_path, settings.local_timeout).await?;
let encryption_key: [u8; 32] = encryption::load_key(&settings)?.into();
let host_id = Settings::host_id().expect("failed to get host_id");
Ok(VarStore::new(sqlite_store, host_id, encryption_key))
}
#[tauri::command]
pub async fn vars() -> Result<Vec<Var>, String> {
let var_store = var_store().await.map_err(|e| e.to_string())?;
let vars = var_store
.vars()
.await
.map_err(|e| format!("failed to load aliases: {}", e))?;
Ok(vars)
}
#[tauri::command]
pub async fn delete_var(name: String) -> Result<(), String> {
let var_store = var_store().await.map_err(|e| e.to_string())?;
var_store
.delete(name.as_str())
.await
.map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
pub async fn set_var(name: String, value: String, export: bool) -> Result<(), String> {
let var_store = var_store().await.map_err(|e| e.to_string())?;
var_store
.set(name.as_str(), value.as_str(), export)
.await
.map_err(|e| e.to_string())?;
Ok(())
}

View File

@ -118,6 +118,9 @@ fn main() {
dotfiles::aliases::import_aliases,
dotfiles::aliases::delete_alias,
dotfiles::aliases::set_alias,
dotfiles::vars::vars,
dotfiles::vars::delete_var,
dotfiles::vars::set_var,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@ -0,0 +1,194 @@
import { useEffect, useState } from "react";
import DataTable from "@/components/ui/data-table";
import { Button } from "@/components/ui/button";
import { MoreHorizontal } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ColumnDef } from "@tanstack/react-table";
import { invoke } from "@tauri-apps/api/core";
import Drawer from "@/components/Drawer";
import { Var } from "@/state/models";
import { useStore } from "@/state/store";
function deleteVar(name: string, refreshVars: () => void) {
invoke("delete_var", { name: name })
.then(() => {
refreshVars();
})
.catch(() => {
console.error("Failed to delete var");
});
}
function AddVar({ onAdd: onAdd }: { onAdd?: () => void }) {
let [name, setName] = useState("");
let [value, setValue] = useState("");
let [exp, setExport] = useState(false);
// simple form to add vars
return (
<div className="p-4">
<h2 className="text-xl font-semibold leading-6 text-gray-900">Add var</h2>
<p className="mt-2">Add a new var to your shell</p>
<form
className="mt-4"
onSubmit={(e) => {
e.preventDefault();
invoke("set_var", { name: name, value: value, export: exp })
.then(() => {
console.log("Added var");
if (onAdd) onAdd();
})
.catch(() => {
console.error("Failed to add var");
});
}}
>
<input
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-md focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Var name"
/>
<input
className="mt-4 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-md focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
autoComplete="off"
autoCapitalize="off"
autoCorrect="off"
spellCheck="false"
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Var value"
/>
<div>
<label>
<input
className="mt-4 bg-gray-50 mr-2 inline"
autoComplete="off"
autoCapitalize="off"
autoCorrect="off"
spellCheck="false"
type="checkbox"
value={exp}
onChange={(e) => setExport(e.target.checked)}
/>
Export the var and make it visible to subprocesses
</label>
</div>
<input
type="submit"
className="block mt-4 rounded-md bg-green-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600"
value="Add var"
/>
</form>
</div>
);
}
export default function Vars() {
const vars = useStore((state) => state.vars);
const refreshVars = useStore((state) => state.refreshVars);
let [varDrawerOpen, setVarDrawerOpen] = useState(false);
const columns: ColumnDef<Var>[] = [
{
accessorKey: "name",
header: "Name",
},
{
accessorKey: "value",
header: "Value",
},
{
id: "actions",
cell: ({ row }: any) => {
const shell_var = row.original;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0 float-right">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4 text-right" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => deleteVar(shell_var.name, refreshVars)}
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
useEffect(() => {
refreshVars();
}, []);
return (
<div className="pt-10">
<div className="sm:flex sm:items-center">
<div className="sm:flex-auto">
<h1 className="text-base font-semibold leading-6 text-gray-900">
Vars
</h1>
<p className="mt-2 text-sm text-gray-700">
Configure environment variables here
</p>
</div>
<div className="mt-4 sm:ml-16 sm:mt-0 flex-row">
<Drawer
open={varDrawerOpen}
onOpenChange={setVarDrawerOpen}
width="30%"
trigger={
<button
type="button"
className="block rounded-md bg-green-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600"
>
Add
</button>
}
>
<AddVar
onAdd={() => {
refreshVars();
setVarDrawerOpen(false);
}}
/>
</Drawer>
</div>
</div>
<div className="mt-8 flow-root">
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<DataTable columns={columns} data={vars} />
</div>
</div>
</div>
</div>
);
}

View File

@ -1,6 +1,35 @@
import { useState } from "react";
import Aliases from "@/components/dotfiles/Aliases";
import Vars from "@/components/dotfiles/Vars";
function Header() {
enum Section {
Aliases,
Vars,
Scripts,
}
function renderDotfiles(current: Section) {
switch (current) {
case Section.Aliases:
return <Aliases />;
case Section.Vars:
return <Vars />;
case Section.Scripts:
return <div />;
}
}
interface HeaderProps {
current: Section;
setCurrent: (section: Section) => void;
}
interface TabsProps {
current: Section;
setCurrent: (section: Section) => void;
}
function Header({ current, setCurrent }: HeaderProps) {
return (
<div className="md:flex md:items-center md:justify-between">
<div className="min-w-0 flex-1">
@ -8,17 +37,72 @@ function Header() {
Dotfiles
</h2>
</div>
<Tabs current={current} setCurrent={setCurrent} />
</div>
);
}
function classNames(...classes) {
return classes.filter(Boolean).join(" ");
}
function Tabs({ current, setCurrent }: TabsProps) {
let tabs = [
{
name: "Aliases",
isCurrent: () => current === Section.Aliases,
section: Section.Aliases,
},
{
name: "Vars",
isCurrent: () => current === Section.Vars,
section: Section.Vars,
},
{
name: "Scripts",
isCurrent: () => current === Section.Scripts,
section: Section.Scripts,
},
];
return (
<div>
<div className="mt-4">
<nav className="flex space-x-4" aria-label="Tabs">
{tabs.map((tab) => (
<button
onClick={() => {
setCurrent(tab.section);
}}
key={tab.name}
className={classNames(
tab.isCurrent()
? "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.isCurrent() ? "page" : undefined}
>
{tab.name}
</button>
))}
</nav>
</div>
</div>
);
}
export default function Dotfiles() {
let [current, setCurrent] = useState(Section.Aliases);
console.log(current);
return (
<div className="pl-60">
<div className="p-10">
<Header />
<Header current={current} setCurrent={setCurrent} />
Manage your shell aliases, variables and paths
<Aliases />
{renderDotfiles(current)}
</div>
</div>
);

View File

@ -32,3 +32,9 @@ export interface Alias {
name: string;
value: string;
}
export interface Var {
name: string;
value: string;
export: bool;
}

View File

@ -19,10 +19,12 @@ interface AtuinState {
user: User;
homeInfo: HomeInfo;
aliases: Alias[];
vars: Var[];
shellHistory: ShellHistory[];
refreshHomeInfo: () => void;
refreshAliases: () => void;
refreshVars: () => void;
refreshShellHistory: (query?: string) => void;
}
@ -30,6 +32,7 @@ export const useStore = create<AtuinState>()((set) => ({
user: DefaultUser,
homeInfo: DefaultHomeInfo,
aliases: [],
vars: [],
shellHistory: [],
refreshAliases: () => {
@ -38,6 +41,12 @@ export const useStore = create<AtuinState>()((set) => ({
});
},
refreshVars: () => {
invoke("vars").then((vars: any) => {
set({ vars: vars });
});
},
refreshShellHistory: (query?: string) => {
if (query) {
invoke("search", { query: query })