diff options
Diffstat (limited to 'ui/src/components')
34 files changed, 0 insertions, 3937 deletions
diff --git a/ui/src/components/Button.tsx b/ui/src/components/Button.tsx deleted file mode 100644 index 5f7e1160..00000000 --- a/ui/src/components/Button.tsx +++ /dev/null @@ -1,20 +0,0 @@ -export enum ButtonStyle { - PrimarySm = "bg-emerald-500 hover:bg-emerald-600", - PrimarySmFill = "bg-emerald-500 hover:bg-emerald-600 w-full text-sm", -} - -interface ButtonProps { - text: string; - style: ButtonStyle; -} - -export default function Button(props: ButtonProps) { - return ( - <button - type="button" - className={`rounded ${props.style} px-2 py-1 font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500`} - > - {props.text} - </button> - ); -} diff --git a/ui/src/components/CodeBlock.tsx b/ui/src/components/CodeBlock.tsx deleted file mode 100644 index 4eb54a1c..00000000 --- a/ui/src/components/CodeBlock.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Highlight, themes } from "prism-react-renderer"; - -// @ts-ignore -import Prism from "prismjs"; - -// @ts-ignore -import "prismjs/components/prism-bash"; - -export default function CodeBlock({ code, language }: any) { - return ( - <div className="overflow-auto"> - <Highlight - theme={themes.github} - code={code} - prism={Prism} - language={language} - > - {({ style, tokens, getLineProps, getTokenProps }) => ( - <pre style={style} className="p-4 break-words whitespace-pre-wrap"> - {tokens.map((line, i) => ( - <div key={i} {...getLineProps({ line })} data-vaul-no-drag> - {i == 0 && ( - <span className="text-gray-500 select-none">$ </span> - )} - {line.map((token, key) => ( - <span - key={key} - {...getTokenProps({ token })} - data-vaul-no-drag - /> - ))} - </div> - ))} - </pre> - )} - </Highlight> - </div> - ); -} diff --git a/ui/src/components/Drawer.tsx b/ui/src/components/Drawer.tsx deleted file mode 100644 index 91753624..00000000 --- a/ui/src/components/Drawer.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Drawer as VDrawer } from "vaul"; - -export default function Drawer({ - trigger, - children, - width, - open, - onOpenChange, -}: any) { - return ( - <VDrawer.Root direction="right" open={open} onOpenChange={onOpenChange}> - <VDrawer.Trigger asChild>{trigger}</VDrawer.Trigger> - <VDrawer.Portal> - <VDrawer.Overlay className="fixed inset-0 bg-black/40 z-50" /> - <VDrawer.Content - style={{ width: width || "400px" }} - className={`bg-white flex flex-col z-50 h-full mt-24 fixed bottom-0 right-0`} - > - {children} - </VDrawer.Content> - </VDrawer.Portal> - </VDrawer.Root> - ); -} diff --git a/ui/src/components/HistoryList.tsx b/ui/src/components/HistoryList.tsx deleted file mode 100644 index 948aa5c9..00000000 --- a/ui/src/components/HistoryList.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import HistoryRow from "./history/HistoryRow"; - -export default function HistoryList(props: any) { - return ( - <div - role="list" - className="divide-y divide-gray-100 bg-white shadow-sm ring-1 ring-gray-900/5 overflow-auto" - style={{ - height: `${props.height}px`, - position: "relative", - }} - > - {props.items.map((i: any) => { - let h = props.history[i.index]; - - return ( - <div - style={{ - position: "absolute", - top: 0, - left: 0, - width: "100%", - height: `${i.size}px`, - transform: `translateY(${i.start}px)`, - }} - > - <HistoryRow h={h} /> - </div> - ); - })} - </div> - ); -} diff --git a/ui/src/components/HistorySearch.tsx b/ui/src/components/HistorySearch.tsx deleted file mode 100644 index 33a3e536..00000000 --- a/ui/src/components/HistorySearch.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { ArrowPathIcon } from "@heroicons/react/24/outline"; -import { MagnifyingGlassIcon } from "@heroicons/react/20/solid"; - -interface HistorySearchProps { - query: string; - refresh: () => void; - setQuery: (query: string) => void; -} - -export default function HistorySearch(props: HistorySearchProps) { - return ( - <div className="flex flex-1 gap-x-4 self-stretch lg:gap-x-6"> - <form - className="relative flex flex-1" - onSubmit={(e) => { - e.preventDefault(); - }} - > - <label htmlFor="search-field" className="sr-only"> - Search - </label> - <MagnifyingGlassIcon - className="pointer-events-none absolute inset-y-0 left-0 h-full w-5 text-gray-400" - aria-hidden="true" - /> - <input - id="search-field" - className="block h-full w-full border-0 py-0 pl-8 pr-0 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm outline-none" - placeholder="Search..." - autoComplete="off" - autoCapitalize="off" - autoCorrect="off" - spellCheck="false" - type="search" - name="search" - onChange={(query) => { - props.setQuery(query.target.value); - }} - /> - </form> - <div className="flex items-center gap-x-4 lg:gap-x-6"> - <button - type="button" - className="-m-2.5 p-2.5 text-gray-400 hover:text-gray-500" - onClick={() => { - props.refresh(); - }} - > - <ArrowPathIcon className="h-6 w-6" aria-hidden="true" /> - </button> - </div> - </div> - ); -} diff --git a/ui/src/components/LoginOrRegister.tsx b/ui/src/components/LoginOrRegister.tsx deleted file mode 100644 index 97f8a790..00000000 --- a/ui/src/components/LoginOrRegister.tsx +++ /dev/null @@ -1,341 +0,0 @@ -import Logo from "@/assets/logo-light.svg"; -import { useState } from "react"; - -import { login, register } from "@/state/client"; -import { useStore } from "@/state/store"; - -interface LoginProps { - toggleRegister: () => void; - onClose: () => void; -} - -function Login(props: LoginProps) { - const refreshUser = useStore((state) => state.refreshUser); - const [errors, setErrors] = useState<string | null>(null); - - const doLogin = async (e: React.FormEvent<HTMLFormElement>) => { - e.preventDefault(); - - const form = e.currentTarget; - const username = form.username.value; - const password = form.password.value; - const key = form.key.value; - - console.log("Logging in..."); - try { - await login(username, password, key); - refreshUser(); - props.onClose(); - } catch (e: any) { - console.error(e); - setErrors(e); - } - }; - - return ( - <> - <div className="flex min-h-full flex-1 flex-col justify-center px-6 "> - <div className="sm:mx-auto sm:w-full sm:max-w-sm"> - <img className="mx-auto h-10 w-auto" src={Logo} alt="Atuin" /> - - <h2 className="mt-5 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900"> - Sign in to your account - </h2> - - <p className="text-sm text-center text-gray-600 mt-4 text-wrap"> - Backup and sync your data across devices. All data is end-to-end - encrypted and stored securely in the cloud. - </p> - </div> - - <div className="mt-10 sm:mx-auto sm:w-full sm:max-w-sm"> - <form - className="space-y-6" - action="#" - method="POST" - onSubmit={doLogin} - > - <div> - <label - htmlFor="username" - className="block text-sm font-medium leading-6 text-gray-900" - > - Username - </label> - <div className="mt-2"> - <input - id="username" - name="username" - type="username" - autoComplete="off" - autoCapitalize="off" - autoCorrect="off" - spellCheck="false" - required - className="block w-full rounded-md border-0 px-1.5 py-1.5 outline-none text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-emerald-600 sm:text-sm sm:leading-6" - /> - </div> - </div> - - <div> - <div className="flex items-center justify-between"> - <label - htmlFor="password" - className="block text-sm font-medium leading-6 text-gray-900" - > - Password - </label> - <div className="text-sm"> - {/* You can't right now. Sorry. Validate emails first. - <a - href="#" - className="font-semibold text-emerald-600 hover:text-emerald-500" - > - Forgot password? - </a> - */} - </div> - </div> - <div className="mt-2"> - <input - id="password" - name="password" - type="password" - autoCapitalize="off" - autoCorrect="off" - spellCheck="false" - autoComplete="current-password" - required - className="block w-full rounded-md border-0 px-1.5 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset outline-none focus:ring-emerald-600 sm:text-sm sm:leading-6" - /> - </div> - </div> - - <div> - <div className="flex items-center justify-between"> - <label - htmlFor="key" - className="block text-sm font-medium leading-6 text-gray-900" - > - <p>Key</p> - <p className="text-xs text-gray-500 font-normal"> - Paste the output of "atuin key" from another machine - </p> - </label> - </div> - <div className="mt-2"> - <input - id="key" - name="key" - autoCapitalize="off" - autoCorrect="off" - spellCheck="false" - autoComplete="off" - required - className="block w-full rounded-md border-0 px-1.5 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset outline-none focus:ring-emerald-600 sm:text-sm sm:leading-6" - /> - </div> - </div> - - <div> - <button - type="submit" - className="flex w-full justify-center rounded-md bg-emerald-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-emerald-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-600" - > - Sign in - </button> - </div> - </form> - - {errors && ( - <p className="mt-4 text-center text-sm text-red-500">{errors}</p> - )} - - <p className="mt-10 text-center text-sm text-gray-500"> - Not a member?{" "} - <a - href="#" - className="font-semibold leading-6 text-emerald-600 hover:text-emerald-500" - onClick={(e) => { - e.preventDefault(); - props.toggleRegister(); - }} - > - Register - </a> - </p> - </div> - </div> - </> - ); -} - -interface RegisterProps { - toggleLogin: () => void; - onClose: () => void; -} - -function Register(props: RegisterProps) { - const refreshUser = useStore((state) => state.refreshUser); - const [errors, setErrors] = useState<string | null>(null); - - const doRegister = async (e: React.FormEvent<HTMLFormElement>) => { - e.preventDefault(); - - const form = e.currentTarget; - const username = form.username.value; - const email = form.email.value; - const password = form.password.value; - - try { - await register(username, email, password); - refreshUser(); - props.onClose(); - } catch (e: any) { - setErrors(e); - } - }; - - return ( - <> - <div className="flex min-h-full flex-1 flex-col justify-center px-6 "> - <div className="sm:mx-auto sm:w-full sm:max-w-sm"> - <img className="mx-auto h-10 w-auto" src={Logo} alt="Atuin" /> - - <h2 className="mt-5 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900"> - Register for an account - </h2> - - <p className="text-sm text-center text-gray-600 mt-4 text-wrap"> - Backup and sync your data across devices. All data is end-to-end - encrypted and stored securely in the cloud. - </p> - </div> - - <div className="mt-10 sm:mx-auto sm:w-full sm:max-w-sm"> - <form - className="space-y-6" - action="#" - method="POST" - onSubmit={doRegister} - > - <div> - <label - htmlFor="username" - className="block text-sm font-medium leading-6 text-gray-900" - > - Username - </label> - <div className="mt-2"> - <input - id="username" - name="username" - type="username" - autoComplete="off" - autoCapitalize="off" - autoCorrect="off" - spellCheck="false" - required - className="block w-full rounded-md border-0 px-1.5 py-1.5 outline-none text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-emerald-600 sm:text-sm sm:leading-6" - /> - </div> - </div> - - <div> - <label - htmlFor="email" - className="block text-sm font-medium leading-6 text-gray-900" - > - Email - </label> - <div className="mt-2"> - <input - id="email" - name="email" - type="email" - autoComplete="email" - autoCapitalize="off" - autoCorrect="off" - spellCheck="false" - required - className="block w-full rounded-md border-0 px-1.5 py-1.5 outline-none text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-emerald-600 sm:text-sm sm:leading-6" - /> - </div> - </div> - - <div> - <div className="flex items-center justify-between"> - <label - htmlFor="password" - className="block text-sm font-medium leading-6 text-gray-900" - > - Password - </label> - <div className="text-sm"> - {/* You can't right now. Sorry. Validate emails first. - <a - href="#" - className="font-semibold text-emerald-600 hover:text-emerald-500" - > - Forgot password? - </a> - */} - </div> - </div> - <div className="mt-2"> - <input - id="password" - name="password" - type="password" - autoCapitalize="off" - autoCorrect="off" - spellCheck="false" - autoComplete="current-password" - required - className="block w-full rounded-md border-0 px-1.5 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset outline-none focus:ring-emerald-600 sm:text-sm sm:leading-6" - /> - </div> - </div> - - <div> - <button - type="submit" - className="flex w-full justify-center rounded-md bg-emerald-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-emerald-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-600" - > - Register - </button> - </div> - </form> - - {errors && ( - <p className="mt-4 text-center text-sm text-red-500">{errors}</p> - )} - - <p className="mt-10 text-center text-sm text-gray-500"> - Already have an account?{" "} - <a - href="#" - className="font-semibold leading-6 text-emerald-600 hover:text-emerald-500" - onClick={(e) => { - e.preventDefault(); - props.toggleLogin(); - }} - > - Login - </a> - </p> - </div> - </div> - </> - ); -} - -export default function LoginOrRegister({ onClose }: { onClose: () => void }) { - let [login, setLogin] = useState<boolean>(false); - - if (login) { - return <Login onClose={onClose} toggleRegister={() => setLogin(false)} />; - } - - return <Register onClose={onClose} toggleLogin={() => setLogin(true)} />; -} diff --git a/ui/src/components/Sidebar/Sidebar.tsx b/ui/src/components/Sidebar/Sidebar.tsx deleted file mode 100644 index 99e2bf82..00000000 --- a/ui/src/components/Sidebar/Sidebar.tsx +++ /dev/null @@ -1,328 +0,0 @@ -"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; diff --git a/ui/src/components/Sidebar/index.tsx b/ui/src/components/Sidebar/index.tsx deleted file mode 100644 index 10020952..00000000 --- a/ui/src/components/Sidebar/index.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import Sidebar, { SidebarItem } from "./Sidebar"; - -export type { SidebarItem }; -export default Sidebar; diff --git a/ui/src/components/dotfiles/Aliases.tsx b/ui/src/components/dotfiles/Aliases.tsx deleted file mode 100644 index 61fd001c..00000000 --- a/ui/src/components/dotfiles/Aliases.tsx +++ /dev/null @@ -1,180 +0,0 @@ -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 { Alias } from "@/state/models"; -import { useStore } from "@/state/store"; - -function deleteAlias(name: string, refreshAliases: () => void) { - invoke("delete_alias", { name: name }) - .then(() => { - refreshAliases(); - }) - .catch(() => { - console.error("Failed to delete alias"); - }); -} - -function AddAlias({ onAdd: onAdd }: { onAdd?: () => void }) { - let [name, setName] = useState(""); - let [value, setValue] = useState(""); - - // simple form to add aliases - return ( - <div className="p-4"> - <h2 className="text-xl font-semibold leading-6 text-gray-900"> - Add alias - </h2> - <p className="mt-2">Add a new alias to your shell</p> - - <form - className="mt-4" - onSubmit={(e) => { - e.preventDefault(); - - invoke("set_alias", { name: name, value: value }) - .then(() => { - console.log("Added alias"); - - if (onAdd) onAdd(); - }) - .catch(() => { - console.error("Failed to add alias"); - }); - }} - > - <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="Alias 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="Alias value" - /> - - <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 alias" - /> - </form> - </div> - ); -} - -export default function Aliases() { - const aliases = useStore((state) => state.aliases); - const refreshAliases = useStore((state) => state.refreshAliases); - - let [aliasDrawerOpen, setAliasDrawerOpen] = useState(false); - - const columns: ColumnDef<Alias>[] = [ - { - accessorKey: "name", - header: "Name", - }, - { - accessorKey: "value", - header: "Value", - }, - { - id: "actions", - cell: ({ row }: any) => { - const alias = 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={() => deleteAlias(alias.name, refreshAliases)} - > - Delete - </DropdownMenuItem> - </DropdownMenuContent> - </DropdownMenu> - ); - }, - }, - ]; - - useEffect(() => { - refreshAliases(); - }, []); - - 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"> - Aliases - </h1> - <p className="mt-2 text-sm text-gray-700"> - Aliases allow you to condense long commands into short, - easy-to-remember commands. - </p> - </div> - <div className="mt-4 sm:ml-16 sm:mt-0 flex-row"> - <Drawer - open={aliasDrawerOpen} - onOpenChange={setAliasDrawerOpen} - 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> - } - > - <AddAlias - onAdd={() => { - refreshAliases(); - setAliasDrawerOpen(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={aliases} /> - </div> - </div> - </div> - </div> - ); -} diff --git a/ui/src/components/dotfiles/Vars.tsx b/ui/src/components/dotfiles/Vars.tsx deleted file mode 100644 index b2379aa7..00000000 --- a/ui/src/components/dotfiles/Vars.tsx +++ /dev/null @@ -1,194 +0,0 @@ -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<boolean>(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.toString()} - 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> - ); -} diff --git a/ui/src/components/history/HistoryInspect.tsx b/ui/src/components/history/HistoryInspect.tsx deleted file mode 100644 index 6c46f2db..00000000 --- a/ui/src/components/history/HistoryInspect.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useState, useEffect } from "react"; - -import PacmanLoader from "react-spinners/PacmanLoader"; - -import CodeBlock from "@/components/CodeBlock"; -import HistoryRow from "@/components/history/HistoryRow"; -import { ShellHistory, inspectCommandHistory } from "@/state/models"; - -function renderLoading() { - return ( - <div className="flex items-center justify-center h-full"> - <PacmanLoader color="#26bd65" /> - </div> - ); -} - -export default function HistoryInspect({ history }: any) { - let [other, setOther] = useState<ShellHistory[]>([]); - - useEffect(() => { - (async () => { - let inspect = await inspectCommandHistory(history); - setOther(inspect.other); - })(); - }, []); - - if (other.length == 0) return renderLoading(); - - return ( - <div className="overflow-y-auto"> - <CodeBlock code={history.command} language="bash" /> - - <div> - {other.map((i: any) => { - return <HistoryRow h={i} />; - })} - </div> - </div> - ); -} diff --git a/ui/src/components/history/HistoryRow.tsx b/ui/src/components/history/HistoryRow.tsx deleted file mode 100644 index 4d893e61..00000000 --- a/ui/src/components/history/HistoryRow.tsx +++ /dev/null @@ -1,120 +0,0 @@ -// @ts-ignore -import { DateTime } from "luxon"; -import { ChevronRightIcon } from "@heroicons/react/20/solid"; -import { Highlight, themes } from "prism-react-renderer"; - -// @ts-ignore -import Prism from "prismjs"; - -// @ts-ignore -import "prismjs/components/prism-bash"; - -import Drawer from "../Drawer"; -import HistoryInspect from "./HistoryInspect"; -import { cn } from "@/lib/utils"; - -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"; -} - -export default function HistoryRow({ h, compact }: any) { - return ( - <li - key={h.id} - className={cn( - "relative flex justify-between gap-x-6 px-4 py-5 hover:bg-gray-50 sm:px-6", - { "py-5": !compact }, - { "py-1": compact }, - )} - > - <div className="flex min-w-0 gap-x-4"> - {!compact && ( - <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 truncate"> - <Highlight - theme={themes.github} - code={h.command} - language="bash" - prism={Prism} - > - {({ style, tokens, getLineProps, getTokenProps }) => ( - <pre style={style} className="!bg-inherit text-sm"> - {tokens && - tokens.map((line, i) => { - if (i != 0) return; - return ( - <div key={i} {...getLineProps({ line })}> - {line.map((token, key) => ( - <span key={key} {...getTokenProps({ token })} /> - ))} - </div> - ); - })} - </pre> - )} - </Highlight> - <p className="mt-1 flex text-xs leading-5 text-gray-500"> - <span className="relative truncate ">{h.user}</span> - - <span> on </span> - - <span className="relative truncate ">{h.host}</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 /> - )} - </div> - <Drawer - width="60%" - trigger={ - <button type="button"> - <ChevronRightIcon - className="h-5 w-5 flex-none text-gray-400" - aria-hidden="true" - /> - </button> - } - > - <HistoryInspect history={h} /> - </Drawer> - </div> - </li> - ); -} diff --git a/ui/src/components/history/Stats.tsx b/ui/src/components/history/Stats.tsx deleted file mode 100644 index f399eaf0..00000000 --- a/ui/src/components/history/Stats.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { useState, useEffect } from "react"; -import { invoke } from "@tauri-apps/api/core"; -import PacmanLoader from "react-spinners/PacmanLoader"; - -import { - BarChart, - Bar, - XAxis, - YAxis, - Tooltip, - ResponsiveContainer, -} from "recharts"; - -function renderLoading() { - return ( - <div className="flex flex-col items-center justify-center h-full "> - <div> - <PacmanLoader color="#26bd65" /> - </div> - <div className="block mt-4"> - <p>Crunching the latest numbers...</p> - </div> - </div> - ); -} - -function TopTable({ stats }: any) { - return ( - <div className="px-4 sm:px-6 lg:px-8"> - <div className="flex items-center"> - <div className="flex-auto"> - <h1 className="text-base font-semibold">Top commands</h1> - </div> - </div> - <div className="mt-4 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"> - <table className="min-w-full divide-y divide-gray-300"> - <thead> - <tr> - <th - scope="col" - className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6 lg:pl-8" - > - Command - </th> - <th - scope="col" - className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900" - > - Count - </th> - </tr> - </thead> - <tbody className="divide-y divide-gray-200 bg-white"> - {stats.map((stat: any) => ( - <tr> - <td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6 lg:pl-8"> - {stat[0][0]} - </td> - <td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500"> - {stat[1]} - </td> - </tr> - ))} - </tbody> - </table> - </div> - </div> - </div> - </div> - ); -} - -export default function Stats() { - const [stats, setStats]: any = useState([]); - const [top, setTop]: any = useState([]); - const [chart, setChart]: any = useState([]); - - useEffect(() => { - if (stats.length != 0) return; - - invoke("global_stats") - .then((s: any) => { - console.log(s.daily); - - setStats([ - { - name: "Total history", - stat: s.total_history.toLocaleString(), - }, - { - name: "Unique history", - stat: s.stats.unique_commands.toLocaleString(), - }, - { - name: "Last 1d", - stat: s.last_1d.toLocaleString(), - }, - { - name: "Last 7d", - stat: s.last_7d.toLocaleString(), - }, - { - name: "Last 30d", - stat: s.last_30d.toLocaleString(), - }, - ]); - - setChart(s.daily); - - setTop(s.stats); - }) - .catch((e) => { - console.log(e); - }); - }, []); - - if (stats.length == 0) { - return renderLoading(); - } - - return ( - <div className="flex flex-col overflow-y-scroll"> - <div className="flexfull"> - <dl className="grid grid-cols-1 sm:grid-cols-5 w-full"> - {stats.map((item: any) => ( - <div - key={item.name} - className="overflow-hidden 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-3xl font-semibold tracking-tight text-gray-900"> - {item.stat} - </dd> - </div> - ))} - </dl> - </div> - - <div className="flex flex-col h-54 py-4 pl-5"> - <div className="flex flex-col h-48 pt-5 pr-5"> - <ResponsiveContainer width="100%" height="100%"> - <BarChart width={500} height={300} data={chart}> - <XAxis dataKey="name" hide={true} /> - <YAxis /> - <Tooltip /> - <Bar dataKey="value" fill="#26bd65" /> - </BarChart> - </ResponsiveContainer> - </div> - </div> - - <div> - <TopTable stats={top.top} /> - </div> - </div> - ); -} diff --git a/ui/src/components/home/QuickActions.tsx b/ui/src/components/home/QuickActions.tsx deleted file mode 100644 index a22e4493..00000000 --- a/ui/src/components/home/QuickActions.tsx +++ /dev/null @@ -1 +0,0 @@ -export default function QuickActions() {} diff --git a/ui/src/components/runbooks/List.tsx b/ui/src/components/runbooks/List.tsx deleted file mode 100644 index 42da3885..00000000 --- a/ui/src/components/runbooks/List.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { useEffect } from "react"; -import { - Button, - ButtonGroup, - Tooltip, - Listbox, - ListboxItem, - Dropdown, - DropdownTrigger, - DropdownMenu, - DropdownItem, - Badge, -} from "@nextui-org/react"; - -import { EllipsisVerticalIcon } from "lucide-react"; - -import { DateTime } from "luxon"; - -import { NotebookPenIcon } from "lucide-react"; -import Runbook from "@/state/runbooks/runbook"; -import { AtuinState, useStore } from "@/state/store"; - -const NoteSidebar = () => { - const runbooks = useStore((state: AtuinState) => state.runbooks); - const refreshRunbooks = useStore( - (state: AtuinState) => state.refreshRunbooks, - ); - - const currentRunbook = useStore((state: AtuinState) => state.currentRunbook); - const setCurrentRunbook = useStore( - (state: AtuinState) => state.setCurrentRunbook, - ); - const runbookInfo = useStore((state: AtuinState) => state.runbookInfo); - - useEffect(() => { - refreshRunbooks(); - }, []); - - return ( - <div className="w-48 flex flex-col border-r-1"> - <div className="overflow-y-auto flex-grow"> - <Listbox - hideSelectedIcon - items={runbooks.map((runbook: any): any => { - return [runbook, runbookInfo[runbook.id]]; - })} - variant="flat" - aria-label="Runbook list" - selectionMode="single" - selectedKeys={currentRunbook ? [currentRunbook] : []} - itemClasses={{ base: "data-[selected=true]:bg-gray-200" }} - topContent={ - <ButtonGroup className="z-20"> - <Tooltip showArrow content="New Runbook" closeDelay={50}> - <Button - isIconOnly - aria-label="New note" - variant="light" - size="sm" - onPress={async () => { - // otherwise the cursor is weirdly positioned in the new document - window.getSelection()?.removeAllRanges(); - - let runbook = await Runbook.create(); - setCurrentRunbook(runbook.id); - refreshRunbooks(); - }} - > - <NotebookPenIcon className="p-[0.15rem]" /> - </Button> - </Tooltip> - </ButtonGroup> - } - > - {([runbook, info]: [Runbook, { ptys: number }]) => ( - <ListboxItem - key={runbook.id} - onPress={() => { - setCurrentRunbook(runbook.id); - }} - textValue={runbook.name || "Untitled"} - endContent={ - <Dropdown> - <Badge - content={info?.ptys} - color="primary" - style={ - info && info?.ptys > 0 - ? {} - : { - display: "none", - } - } - > - <DropdownTrigger className="bg-transparent"> - <Button isIconOnly> - <EllipsisVerticalIcon - size="16px" - className="bg-transparent" - /> - </Button> - </DropdownTrigger> - </Badge> - <DropdownMenu aria-label="Dynamic Actions"> - <DropdownItem - key={"delete"} - color="danger" - className="text-danger" - onPress={async () => { - await Runbook.delete(runbook.id); - - if (runbook.id == currentRunbook) setCurrentRunbook(""); - - refreshRunbooks(); - }} - > - Delete - </DropdownItem> - </DropdownMenu> - </Dropdown> - } - > - <div className="flex flex-col"> - <div className="text-md">{runbook.name || "Untitled"}</div> - <div className="text-xs text-gray-500"> - <em> - {DateTime.fromJSDate(runbook.updated).toLocaleString( - DateTime.DATETIME_SHORT, - )} - </em> - </div> - </div> - </ListboxItem> - )} - </Listbox> - </div> - </div> - ); -}; - -export default NoteSidebar; diff --git a/ui/src/components/runbooks/editor/Editor.tsx b/ui/src/components/runbooks/editor/Editor.tsx deleted file mode 100644 index 6b0522f5..00000000 --- a/ui/src/components/runbooks/editor/Editor.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; - -import "./index.css"; - -import { Spinner } from "@nextui-org/react"; - -// Errors, but it all works fine and is there. Maybe missing ts defs? -// I'll figure it out later -import { - // @ts-ignore - BlockNoteSchema, - // @ts-ignore - BlockNoteEditor, - // @ts-ignore - defaultBlockSpecs, - // @ts-ignore - filterSuggestionItems, - // @ts-ignore - insertOrUpdateBlock, -} from "@blocknote/core"; - -import { - //@ts-ignore - SuggestionMenuController, - // @ts-ignore - AddBlockButton, - // @ts-ignore - getDefaultReactSlashMenuItems, - // @ts-ignore - SideMenu, - // @ts-ignore - SideMenuController, -} from "@blocknote/react"; -import { BlockNoteView } from "@blocknote/mantine"; - -import "@blocknote/core/fonts/inter.css"; -import "@blocknote/mantine/style.css"; - -import { CodeIcon, FolderOpenIcon } from "lucide-react"; -import { useDebounceCallback } from "usehooks-ts"; - -import Run from "@/components/runbooks/editor/blocks/Run"; -import Directory from "@/components/runbooks/editor/blocks/Directory"; - -import { DeleteBlock } from "@/components/runbooks/editor/ui/DeleteBlockButton"; -import { AtuinState, useStore } from "@/state/store"; -import Runbook from "@/state/runbooks/runbook"; - -// Our schema with block specs, which contain the configs and implementations for blocks -// that we want our editor to use. -const schema = BlockNoteSchema.create({ - blockSpecs: { - // Adds all default blocks. - ...defaultBlockSpecs, - - // Adds the code block. - run: Run, - directory: Directory, - }, -}); - -// Slash menu item to insert an Alert block -const insertRun = (editor: typeof schema.BlockNoteEditor) => ({ - title: "Code", - onItemClick: () => { - insertOrUpdateBlock(editor, { - type: "run", - }); - }, - icon: <CodeIcon size={18} />, - aliases: ["code", "run"], - group: "Execute", -}); - -const insertDirectory = (editor: typeof schema.BlockNoteEditor) => ({ - title: "Directory", - onItemClick: () => { - insertOrUpdateBlock(editor, { - type: "directory", - }); - }, - icon: <FolderOpenIcon size={18} />, - aliases: ["directory", "dir", "folder"], - group: "Execute", -}); - -export default function Editor() { - const runbookId = useStore((store: AtuinState) => store.currentRunbook); - const refreshRunbooks = useStore( - (store: AtuinState) => store.refreshRunbooks, - ); - let [runbook, setRunbook] = useState<Runbook | null>(null); - - useEffect(() => { - if (!runbookId) return; - - const fetchRunbook = async () => { - let rb = await Runbook.load(runbookId); - - setRunbook(rb); - }; - - fetchRunbook(); - }, [runbookId]); - - const onChange = async () => { - if (!runbook) return; - - console.log("saved!"); - runbook.name = fetchName(); - if (editor) runbook.content = JSON.stringify(editor.document); - - await runbook.save(); - refreshRunbooks(); - }; - - const debouncedOnChange = useDebounceCallback(onChange, 1000); - - const editor = useMemo(() => { - if (!runbook) return undefined; - if (runbook.content) { - return BlockNoteEditor.create({ - initialContent: JSON.parse(runbook.content), - schema, - }); - } - - return BlockNoteEditor.create({ schema }); - }, [runbook]); - - const fetchName = (): string => { - // Infer the title from the first text block - if (!editor) return "Untitled"; - - let blocks = editor.document; - for (const block of blocks) { - if (block.type == "heading" || block.type == "paragraph") { - if (block.content.length == 0) continue; - // @ts-ignore - if (block.content[0].text.length == 0) continue; - - // @ts-ignore - return block.content[0].text; - } - } - - return "Untitled"; - }; - - if (!runbook) { - return ( - <div className="flex w-full h-full flex-col justify-center items-center"> - <Spinner /> - </div> - ); - } - - if (editor === undefined) { - return ( - <div className="flex w-full h-full flex-col justify-center items-center"> - <Spinner /> - </div> - ); - } - - // Renders the editor instance. - return ( - <div className="overflow-y-scroll w-full"> - <BlockNoteView - editor={editor} - slashMenu={false} - sideMenu={false} - onChange={debouncedOnChange} - > - <SuggestionMenuController - triggerCharacter={"/"} - getItems={async (query: any) => - filterSuggestionItems( - [ - ...getDefaultReactSlashMenuItems(editor), - insertRun(editor), - insertDirectory(editor), - ], - query, - ) - } - /> - - <SideMenuController - sideMenu={(props: any) => ( - <SideMenu {...props}> - <AddBlockButton {...props} /> - <DeleteBlock {...props} /> - </SideMenu> - )} - /> - </BlockNoteView> - </div> - ); -} diff --git a/ui/src/components/runbooks/editor/blocks/Directory/index.tsx b/ui/src/components/runbooks/editor/blocks/Directory/index.tsx deleted file mode 100644 index 3e4f93d9..00000000 --- a/ui/src/components/runbooks/editor/blocks/Directory/index.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { useState } from "react"; -import { Input, Tooltip, Button } from "@nextui-org/react"; -import { FolderInputIcon } from "lucide-react"; - -// @ts-ignore -import { createReactBlockSpec } from "@blocknote/react"; - -import { open } from "@tauri-apps/plugin-dialog"; - -interface DirectoryProps { - path: string; - onInputChange: (val: string) => void; -} - -const Directory = ({ path, onInputChange }: DirectoryProps) => { - const [value, setValue] = useState(path); - - const selectFolder = async () => { - const path = await open({ - multiple: false, - directory: true, - }); - - setValue(path || ""); - onInputChange(path || ""); - }; - - return ( - <div className="w-full !max-w-full !outline-none overflow-none"> - <Tooltip - content="Change working directory for all subsequent code blocks" - delay={1000} - > - <div className="flex flex-row"> - <div className="mr-2"> - <Button - isIconOnly - variant="flat" - aria-label="Select folder" - onPress={selectFolder} - > - <FolderInputIcon /> - </Button> - </div> - - <div className="w-full"> - <Input - placeholder="~" - value={value} - autoComplete="off" - autoCapitalize="off" - autoCorrect="off" - spellCheck="false" - onValueChange={(val) => { - setValue(val); - onInputChange(val); - }} - /> - </div> - </div> - </Tooltip> - </div> - ); -}; - -export default createReactBlockSpec( - { - type: "directory", - propSchema: { - path: { default: "" }, - }, - content: "none", - }, - { - // @ts-ignore - render: ({ block, editor, code, type }) => { - const onInputChange = (val: string) => { - editor.updateBlock(block, { - // @ts-ignore - props: { ...block.props, path: val }, - }); - }; - - return ( - <Directory path={block.props.path} onInputChange={onInputChange} /> - ); - }, - }, -); diff --git a/ui/src/components/runbooks/editor/blocks/Run/extensions.ts b/ui/src/components/runbooks/editor/blocks/Run/extensions.ts deleted file mode 100644 index 76fc4343..00000000 --- a/ui/src/components/runbooks/editor/blocks/Run/extensions.ts +++ /dev/null @@ -1,158 +0,0 @@ -// Based on the basicSetup extension, as suggested by the source. Customized for Atuin. - -import { - KeyBinding, - lineNumbers, - highlightActiveLineGutter, - highlightSpecialChars, - drawSelection, - dropCursor, - rectangularSelection, - crosshairCursor, - highlightActiveLine, - keymap, -} from "@codemirror/view"; -import { EditorState, Extension } from "@codemirror/state"; -import { history, defaultKeymap, historyKeymap } from "@codemirror/commands"; -import { highlightSelectionMatches, searchKeymap } from "@codemirror/search"; - -import { - closeBrackets, - autocompletion, - closeBracketsKeymap, - completionKeymap, - CompletionContext, -} from "@codemirror/autocomplete"; - -import { - foldGutter, - indentOnInput, - syntaxHighlighting, - defaultHighlightStyle, - bracketMatching, - indentUnit, - foldKeymap, -} from "@codemirror/language"; - -import { lintKeymap } from "@codemirror/lint"; -import { invoke } from "@tauri-apps/api/core"; - -export interface MinimalSetupOptions { - highlightSpecialChars?: boolean; - history?: boolean; - drawSelection?: boolean; - syntaxHighlighting?: boolean; - - defaultKeymap?: boolean; - historyKeymap?: boolean; -} - -export interface BasicSetupOptions extends MinimalSetupOptions { - lineNumbers?: boolean; - highlightActiveLineGutter?: boolean; - foldGutter?: boolean; - dropCursor?: boolean; - allowMultipleSelections?: boolean; - indentOnInput?: boolean; - bracketMatching?: boolean; - closeBrackets?: boolean; - autocompletion?: boolean; - rectangularSelection?: boolean; - crosshairCursor?: boolean; - highlightActiveLine?: boolean; - highlightSelectionMatches?: boolean; - - closeBracketsKeymap?: boolean; - searchKeymap?: boolean; - foldKeymap?: boolean; - completionKeymap?: boolean; - lintKeymap?: boolean; - tabSize?: number; -} - -function myCompletions(context: CompletionContext) { - let word = context.matchBefore(/^.*/); - - if (!word) return null; - if (word.from == word.to && !context.explicit) return null; - - return invoke("prefix_search", { query: word.text }).then( - // @ts-ignore - (results: string[]) => { - let options = results.map((i) => { - return { label: i, type: "text" }; - }); - - return { - from: word.from, - options, - }; - }, - ); -} - -const buildAutocomplete = (): Extension => { - let ac = autocompletion({ override: [myCompletions] }); - - return ac; -}; - -export const extensions = (options: BasicSetupOptions = {}): Extension[] => { - const { crosshairCursor: initCrosshairCursor = false } = options; - - let keymaps: KeyBinding[] = []; - if (options.closeBracketsKeymap !== false) { - keymaps = keymaps.concat(closeBracketsKeymap); - } - if (options.defaultKeymap !== false) { - keymaps = keymaps.concat(defaultKeymap); - } - if (options.searchKeymap !== false) { - keymaps = keymaps.concat(searchKeymap); - } - if (options.historyKeymap !== false) { - keymaps = keymaps.concat(historyKeymap); - } - if (options.foldKeymap !== false) { - keymaps = keymaps.concat(foldKeymap); - } - if (options.completionKeymap !== false) { - keymaps = keymaps.concat(completionKeymap); - } - if (options.lintKeymap !== false) { - keymaps = keymaps.concat(lintKeymap); - } - const extensions: Extension[] = []; - if (options.lineNumbers !== false) extensions.push(lineNumbers()); - if (options.highlightActiveLineGutter !== false) - extensions.push(highlightActiveLineGutter()); - if (options.highlightSpecialChars !== false) - extensions.push(highlightSpecialChars()); - if (options.history !== false) extensions.push(history()); - if (options.foldGutter !== false) extensions.push(foldGutter()); - if (options.drawSelection !== false) extensions.push(drawSelection()); - if (options.dropCursor !== false) extensions.push(dropCursor()); - if (options.allowMultipleSelections !== false) - extensions.push(EditorState.allowMultipleSelections.of(true)); - if (options.indentOnInput !== false) extensions.push(indentOnInput()); - if (options.syntaxHighlighting !== false) - extensions.push( - syntaxHighlighting(defaultHighlightStyle, { fallback: true }), - ); - - if (options.bracketMatching !== false) extensions.push(bracketMatching()); - if (options.closeBrackets !== false) extensions.push(closeBrackets()); - if (options.autocompletion !== false) extensions.push(buildAutocomplete()); - - if (options.rectangularSelection !== false) - extensions.push(rectangularSelection()); - if (initCrosshairCursor !== false) extensions.push(crosshairCursor()); - if (options.highlightActiveLine !== false) - extensions.push(highlightActiveLine()); - if (options.highlightSelectionMatches !== false) - extensions.push(highlightSelectionMatches()); - if (options.tabSize && typeof options.tabSize === "number") - extensions.push(indentUnit.of(" ".repeat(options.tabSize))); - - return extensions.concat([keymap.of(keymaps.flat())]).filter(Boolean); -}; diff --git a/ui/src/components/runbooks/editor/blocks/Run/index.css b/ui/src/components/runbooks/editor/blocks/Run/index.css deleted file mode 100644 index e854c03b..00000000 --- a/ui/src/components/runbooks/editor/blocks/Run/index.css +++ /dev/null @@ -1,9 +0,0 @@ -ProseMirror-focused { - outline: none !important; - box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1) !important; -} - -.cm-editor.cm-focused { - outline: none !important; - box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1) !important; -} diff --git a/ui/src/components/runbooks/editor/blocks/Run/index.tsx b/ui/src/components/runbooks/editor/blocks/Run/index.tsx deleted file mode 100644 index bef083ba..00000000 --- a/ui/src/components/runbooks/editor/blocks/Run/index.tsx +++ /dev/null @@ -1,229 +0,0 @@ -// @ts-ignore -import { createReactBlockSpec } from "@blocknote/react"; - -import "./index.css"; - -import CodeMirror from "@uiw/react-codemirror"; -import { keymap } from "@codemirror/view"; -import { langs } from "@uiw/codemirror-extensions-langs"; - -import { Play, Square } from "lucide-react"; -import { useState } from "react"; - -import { extensions } from "./extensions"; -import { platform } from "@tauri-apps/plugin-os"; -import { invoke } from "@tauri-apps/api/core"; -import Terminal from "./terminal.tsx"; - -import "@xterm/xterm/css/xterm.css"; -import { AtuinState, useStore } from "@/state/store.ts"; - -interface RunBlockProps { - onChange: (val: string) => void; - onRun?: (pty: string) => void; - onStop?: (pty: string) => void; - id: string; - code: string; - type: string; - pty: string; - isEditable: boolean; - editor: any; -} - -const findFirstParentOfType = (editor: any, id: string, type: string): any => { - // TODO: the types for blocknote aren't working. Now I'm doing this sort of shit, - // really need to fix that. - const document = editor.document; - var lastOfType = null; - - // Iterate through ALL of the blocks. - for (let i = 0; i < document.length; i++) { - if (document[i].id == id) return lastOfType; - - if (document[i].type == type) lastOfType = document[i]; - } - - return lastOfType; -}; - -const RunBlock = ({ - onChange, - id, - code, - isEditable, - onRun, - onStop, - pty, - editor, -}: RunBlockProps) => { - const [value, setValue] = useState<String>(code); - const cleanupPtyTerm = useStore((store: AtuinState) => store.cleanupPtyTerm); - const terminals = useStore((store: AtuinState) => store.terminals); - - const [currentRunbook, incRunbookPty, decRunbookPty] = useStore( - (store: AtuinState) => [ - store.currentRunbook, - store.incRunbookPty, - store.decRunbookPty, - ], - ); - - const isRunning = pty !== null && pty !== ""; - - const handleToggle = async (event: any | null) => { - if (event) event.stopPropagation(); - - // If there's no code, don't do anything - if (!value) return; - - if (isRunning) { - await invoke("pty_kill", { pid: pty }); - - terminals[pty].terminal.dispose(); - cleanupPtyTerm(pty); - - if (onStop) onStop(pty); - if (currentRunbook) decRunbookPty(currentRunbook); - } - - if (!isRunning) { - let cwd = findFirstParentOfType(editor, id, "directory"); - - if (cwd) { - cwd = cwd.props.path; - } else { - cwd = "~"; - } - - let pty = await invoke<string>("pty_open", { cwd }); - if (onRun) onRun(pty); - - if (currentRunbook) incRunbookPty(currentRunbook); - - let isWindows = platform() == "windows"; - let cmdEnd = isWindows ? "\r\n" : "\n"; - - let val = !value.endsWith("\n") ? value + cmdEnd : value; - await invoke("pty_write", { pid: pty, data: val }); - } - }; - - const handleCmdEnter = () => { - handleToggle(null); - return true; - }; - - const customKeymap = keymap.of([ - { - key: "Mod-Enter", - run: handleCmdEnter, - }, - ]); - - return ( - <div className="w-full !max-w-full !outline-none overflow-none"> - <div className="flex flex-row items-start"> - <div className="flex"> - <button - onClick={handleToggle} - className={`flex items-center justify-center flex-shrink-0 w-8 h-8 mr-2 rounded border focus:outline-none focus:ring-2 transition-all duration-300 ease-in-out ${ - isRunning - ? "border-red-200 bg-red-50 text-red-600 hover:bg-red-100 hover:border-red-300 focus:ring-red-300" - : "border-green-200 bg-green-50 text-green-600 hover:bg-green-100 hover:border-green-300 focus:ring-green-300" - }`} - aria-label={isRunning ? "Stop code" : "Run code"} - > - <span - className={`inline-block transition-transform duration-300 ease-in-out ${isRunning ? "rotate-180" : ""}`} - > - {isRunning ? <Square size={16} /> : <Play size={16} />} - </span> - </button> - </div> - <div className="flex-1 min-w-0 w-40"> - <CodeMirror - id={id} - placeholder={"Write your code here..."} - className="!pt-0 max-w-full border border-gray-300 rounded" - value={code} - editable={isEditable} - autoFocus - onChange={(val) => { - setValue(val); - onChange(val); - }} - extensions={[customKeymap, ...extensions(), langs.shell()]} - basicSetup={false} - /> - <div - className={`overflow-hidden transition-all duration-300 ease-in-out min-w-0 ${ - isRunning ? "block" : "hidden" - }`} - > - {pty && <Terminal pty={pty} />} - </div> - </div> - </div> - </div> - ); -}; - -export default createReactBlockSpec( - { - type: "run", - propSchema: { - type: { - default: "bash", - }, - code: { default: "" }, - pty: { default: "" }, - global: { default: false }, - }, - content: "none", - }, - { - // @ts-ignore - render: ({ block, editor, code, type }) => { - const onInputChange = (val: string) => { - editor.updateBlock(block, { - // @ts-ignore - props: { ...block.props, code: val }, - }); - }; - - const onRun = (pty: string) => { - editor.updateBlock(block, { - // @ts-ignore - props: { ...block.props, pty: pty }, - }); - }; - - const onStop = (_pty: string) => { - editor?.updateBlock(block, { - props: { ...block.props, pty: "" }, - }); - }; - - return ( - <RunBlock - onChange={onInputChange} - id={block?.id} - code={block.props.code} - type={block.props.type} - pty={block.props.pty} - isEditable={editor.isEditable} - onRun={onRun} - onStop={onStop} - editor={editor} - /> - ); - }, - toExternalHTML: ({ block }) => { - return ( - <pre lang="beep boop"> - <code lang="bash">{block?.props?.code}</code> - </pre> - ); - }, - }, -); diff --git a/ui/src/components/runbooks/editor/blocks/Run/terminal.tsx b/ui/src/components/runbooks/editor/blocks/Run/terminal.tsx deleted file mode 100644 index a6dc589f..00000000 --- a/ui/src/components/runbooks/editor/blocks/Run/terminal.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { useState, useEffect, useRef } from "react"; -import { listen } from "@tauri-apps/api/event"; -import "@xterm/xterm/css/xterm.css"; -import { useStore } from "@/state/store"; -import { invoke } from "@tauri-apps/api/core"; -import { IDisposable } from "@xterm/xterm"; - -const usePersistentTerminal = (pty: string) => { - const newPtyTerm = useStore((store) => store.newPtyTerm); - const terminals = useStore((store) => store.terminals); - const [isReady, setIsReady] = useState(false); - - useEffect(() => { - if (!terminals.hasOwnProperty(pty)) { - // create a new terminal and store it in the store. - // this means we can resume the same instance even across mount/dismount - newPtyTerm(pty); - } - - setIsReady(true); - - return () => { - // We don't dispose of the terminal when the component unmounts - }; - }, [pty, terminals, newPtyTerm]); - - return { terminalData: terminals[pty], isReady }; -}; - -const TerminalComponent = ({ pty }: any) => { - const terminalRef = useRef(null); - const { terminalData, isReady } = usePersistentTerminal(pty); - const [isAttached, setIsAttached] = useState(false); - const cleanupListenerRef = useRef<(() => void) | null>(null); - const keyDispose = useRef<IDisposable | null>(null); - - useEffect(() => { - // no pty? no terminal - if (pty == null) return; - - // the terminal may still be being created so hold off - if (!isReady) return; - - const windowResize = () => { - if (!terminalData || !terminalData.fitAddon) return; - - terminalData.fitAddon.fit(); - }; - - // terminal object needs attaching to a ref to a div - if (!isAttached && terminalData && terminalData.terminal) { - // If it's never been attached, attach it - if (!terminalData.terminal.element && terminalRef.current) { - terminalData.terminal.open(terminalRef.current); - - // it might have been previously attached, but need moving elsewhere - } else if (terminalData && terminalRef.current) { - // @ts-ignore - terminalRef.current.appendChild(terminalData.terminal.element); - } - - terminalData.fitAddon.fit(); - setIsAttached(true); - - window.addEventListener("resize", windowResize); - - const disposeOnKey = terminalData.terminal.onKey(async (event) => { - await invoke("pty_write", { pid: pty, data: event.key }); - }); - - keyDispose.current = disposeOnKey; - } - - listen(`pty-${pty}`, (event: any) => { - terminalData.terminal.write(event.payload); - }).then((ul) => { - cleanupListenerRef.current = ul; - }); - - // Customize further as needed - return () => { - if ( - terminalData && - terminalData.terminal && - terminalData.terminal.element - ) { - // Instead of removing, we just detach - if (terminalData.terminal.element.parentElement) { - terminalData.terminal.element.parentElement.removeChild( - terminalData.terminal.element, - ); - } - setIsAttached(false); - } - - if (cleanupListenerRef.current) { - cleanupListenerRef.current(); - } - - if (keyDispose.current) keyDispose.current.dispose(); - - window.removeEventListener("resize", windowResize); - }; - }, [terminalData, isReady]); - - if (!isReady) return null; - - return ( - <div className="!max-w-full min-w-0 overflow-hidden" ref={terminalRef} /> - ); -}; - -export default TerminalComponent; diff --git a/ui/src/components/runbooks/editor/index.css b/ui/src/components/runbooks/editor/index.css deleted file mode 100644 index 067cc500..00000000 --- a/ui/src/components/runbooks/editor/index.css +++ /dev/null @@ -1,7 +0,0 @@ -.editor a { - color: #0000ee; -} - -.editor a:hover { - cursor: pointer; -} diff --git a/ui/src/components/runbooks/editor/ui/DeleteBlockButton.tsx b/ui/src/components/runbooks/editor/ui/DeleteBlockButton.tsx deleted file mode 100644 index 84a9f5c8..00000000 --- a/ui/src/components/runbooks/editor/ui/DeleteBlockButton.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { - SideMenuProps, - useBlockNoteEditor, - useComponentsContext, -} from "@blocknote/react"; -import { TrashIcon } from "lucide-react"; - -// Custom Side Menu button to remove the hovered block. -export function DeleteBlock(props: SideMenuProps) { - const editor = useBlockNoteEditor(); - - const Components = useComponentsContext()!; - - return ( - <Components.SideMenu.Button - label="Remove block" - className="mx-1" - icon={ - <TrashIcon - size={24} - onClick={() => { - editor.removeBlocks([props.block]); - }} - /> - } - /> - ); -} diff --git a/ui/src/components/ui/alert.tsx b/ui/src/components/ui/alert.tsx deleted file mode 100644 index 41fa7e05..00000000 --- a/ui/src/components/ui/alert.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const alertVariants = cva( - "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", - { - variants: { - variant: { - default: "bg-background text-foreground", - destructive: - "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -const Alert = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants> ->(({ className, variant, ...props }, ref) => ( - <div - ref={ref} - role="alert" - className={cn(alertVariants({ variant }), className)} - {...props} - /> -)) -Alert.displayName = "Alert" - -const AlertTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes<HTMLHeadingElement> ->(({ className, ...props }, ref) => ( - <h5 - ref={ref} - className={cn("mb-1 font-medium leading-none tracking-tight", className)} - {...props} - /> -)) -AlertTitle.displayName = "AlertTitle" - -const AlertDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes<HTMLParagraphElement> ->(({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn("text-sm [&_p]:leading-relaxed", className)} - {...props} - /> -)) -AlertDescription.displayName = "AlertDescription" - -export { Alert, AlertTitle, AlertDescription } diff --git a/ui/src/components/ui/button.tsx b/ui/src/components/ui/button.tsx deleted file mode 100644 index 0ba42773..00000000 --- a/ui/src/components/ui/button.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", - destructive: - "bg-destructive text-destructive-foreground hover:bg-destructive/90", - outline: - "border border-input bg-background hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-10 px-4 py-2", - sm: "h-9 rounded-md px-3", - lg: "h-11 rounded-md px-8", - icon: "h-10 w-10", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) - -export interface ButtonProps - extends React.ButtonHTMLAttributes<HTMLButtonElement>, - VariantProps<typeof buttonVariants> { - asChild?: boolean -} - -const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button" - return ( - <Comp - className={cn(buttonVariants({ variant, size, className }))} - ref={ref} - {...props} - /> - ) - } -) -Button.displayName = "Button" - -export { Button, buttonVariants } diff --git a/ui/src/components/ui/card.tsx b/ui/src/components/ui/card.tsx deleted file mode 100644 index afa13ecf..00000000 --- a/ui/src/components/ui/card.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import * as React from "react" - -import { cn } from "@/lib/utils" - -const Card = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> ->(({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn( - "rounded-lg border bg-card text-card-foreground shadow-sm", - className - )} - {...props} - /> -)) -Card.displayName = "Card" - -const CardHeader = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> ->(({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn("flex flex-col space-y-1.5 p-6", className)} - {...props} - /> -)) -CardHeader.displayName = "CardHeader" - -const CardTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes<HTMLHeadingElement> ->(({ className, ...props }, ref) => ( - <h3 - ref={ref} - className={cn( - "text-2xl font-semibold leading-none tracking-tight", - className - )} - {...props} - /> -)) -CardTitle.displayName = "CardTitle" - -const CardDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes<HTMLParagraphElement> ->(({ className, ...props }, ref) => ( - <p - ref={ref} - className={cn("text-sm text-muted-foreground", className)} - {...props} - /> -)) -CardDescription.displayName = "CardDescription" - -const CardContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> ->(({ className, ...props }, ref) => ( - <div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> -)) -CardContent.displayName = "CardContent" - -const CardFooter = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> ->(({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn("flex items-center p-6 pt-0", className)} - {...props} - /> -)) -CardFooter.displayName = "CardFooter" - -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/ui/src/components/ui/chart.tsx b/ui/src/components/ui/chart.tsx deleted file mode 100644 index a21d77ee..00000000 --- a/ui/src/components/ui/chart.tsx +++ /dev/null @@ -1,363 +0,0 @@ -import * as React from "react" -import * as RechartsPrimitive from "recharts" - -import { cn } from "@/lib/utils" - -// Format: { THEME_NAME: CSS_SELECTOR } -const THEMES = { light: "", dark: ".dark" } as const - -export type ChartConfig = { - [k in string]: { - label?: React.ReactNode - icon?: React.ComponentType - } & ( - | { color?: string; theme?: never } - | { color?: never; theme: Record<keyof typeof THEMES, string> } - ) -} - -type ChartContextProps = { - config: ChartConfig -} - -const ChartContext = React.createContext<ChartContextProps | null>(null) - -function useChart() { - const context = React.useContext(ChartContext) - - if (!context) { - throw new Error("useChart must be used within a <ChartContainer />") - } - - return context -} - -const ChartContainer = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> & { - config: ChartConfig - children: React.ComponentProps< - typeof RechartsPrimitive.ResponsiveContainer - >["children"] - } ->(({ id, className, children, config, ...props }, ref) => { - const uniqueId = React.useId() - const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` - - return ( - <ChartContext.Provider value={{ config }}> - <div - data-chart={chartId} - ref={ref} - className={cn( - "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none", - className - )} - {...props} - > - <ChartStyle id={chartId} config={config} /> - <RechartsPrimitive.ResponsiveContainer> - {children} - </RechartsPrimitive.ResponsiveContainer> - </div> - </ChartContext.Provider> - ) -}) -ChartContainer.displayName = "Chart" - -const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { - const colorConfig = Object.entries(config).filter( - ([_, config]) => config.theme || config.color - ) - - if (!colorConfig.length) { - return null - } - - return ( - <style - dangerouslySetInnerHTML={{ - __html: Object.entries(THEMES) - .map( - ([theme, prefix]) => ` -${prefix} [data-chart=${id}] { -${colorConfig - .map(([key, itemConfig]) => { - const color = - itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || - itemConfig.color - return color ? ` --color-${key}: ${color};` : null - }) - .join("\n")} -} -` - ) - .join("\n"), - }} - /> - ) -} - -const ChartTooltip = RechartsPrimitive.Tooltip - -const ChartTooltipContent = React.forwardRef< - HTMLDivElement, - React.ComponentProps<typeof RechartsPrimitive.Tooltip> & - React.ComponentProps<"div"> & { - hideLabel?: boolean - hideIndicator?: boolean - indicator?: "line" | "dot" | "dashed" - nameKey?: string - labelKey?: string - } ->( - ( - { - active, - payload, - className, - indicator = "dot", - hideLabel = false, - hideIndicator = false, - label, - labelFormatter, - labelClassName, - formatter, - color, - nameKey, - labelKey, - }, - ref - ) => { - const { config } = useChart() - - const tooltipLabel = React.useMemo(() => { - if (hideLabel || !payload?.length) { - return null - } - - const [item] = payload - const key = `${labelKey || item.dataKey || item.name || "value"}` - const itemConfig = getPayloadConfigFromPayload(config, item, key) - const value = - !labelKey && typeof label === "string" - ? config[label as keyof typeof config]?.label || label - : itemConfig?.label - - if (labelFormatter) { - return ( - <div className={cn("font-medium", labelClassName)}> - {labelFormatter(value, payload)} - </div> - ) - } - - if (!value) { - return null - } - - return <div className={cn("font-medium", labelClassName)}>{value}</div> - }, [ - label, - labelFormatter, - payload, - hideLabel, - labelClassName, - config, - labelKey, - ]) - - if (!active || !payload?.length) { - return null - } - - const nestLabel = payload.length === 1 && indicator !== "dot" - - return ( - <div - ref={ref} - className={cn( - "grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl", - className - )} - > - {!nestLabel ? tooltipLabel : null} - <div className="grid gap-1.5"> - {payload.map((item, index) => { - const key = `${nameKey || item.name || item.dataKey || "value"}` - const itemConfig = getPayloadConfigFromPayload(config, item, key) - const indicatorColor = color || item.payload.fill || item.color - - return ( - <div - key={item.dataKey} - className={cn( - "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground", - indicator === "dot" && "items-center" - )} - > - {formatter && item?.value !== undefined && item.name ? ( - formatter(item.value, item.name, item, index, item.payload) - ) : ( - <> - {itemConfig?.icon ? ( - <itemConfig.icon /> - ) : ( - !hideIndicator && ( - <div - className={cn( - "shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", - { - "h-2.5 w-2.5": indicator === "dot", - "w-1": indicator === "line", - "w-0 border-[1.5px] border-dashed bg-transparent": - indicator === "dashed", - "my-0.5": nestLabel && indicator === "dashed", - } - )} - style={ - { - "--color-bg": indicatorColor, - "--color-border": indicatorColor, - } as React.CSSProperties - } - /> - ) - )} - <div - className={cn( - "flex flex-1 justify-between leading-none", - nestLabel ? "items-end" : "items-center" - )} - > - <div className="grid gap-1.5"> - {nestLabel ? tooltipLabel : null} - <span className="text-muted-foreground"> - {itemConfig?.label || item.name} - </span> - </div> - {item.value && ( - <span className="font-mono font-medium tabular-nums text-foreground"> - {item.value.toLocaleString()} - </span> - )} - </div> - </> - )} - </div> - ) - })} - </div> - </div> - ) - } -) -ChartTooltipContent.displayName = "ChartTooltip" - -const ChartLegend = RechartsPrimitive.Legend - -const ChartLegendContent = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> & - Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & { - hideIcon?: boolean - nameKey?: string - } ->( - ( - { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, - ref - ) => { - const { config } = useChart() - - if (!payload?.length) { - return null - } - - return ( - <div - ref={ref} - className={cn( - "flex items-center justify-center gap-4", - verticalAlign === "top" ? "pb-3" : "pt-3", - className - )} - > - {payload.map((item) => { - const key = `${nameKey || item.dataKey || "value"}` - const itemConfig = getPayloadConfigFromPayload(config, item, key) - - return ( - <div - key={item.value} - className={cn( - "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground" - )} - > - {itemConfig?.icon && !hideIcon ? ( - <itemConfig.icon /> - ) : ( - <div - className="h-2 w-2 shrink-0 rounded-[2px]" - style={{ - backgroundColor: item.color, - }} - /> - )} - {itemConfig?.label} - </div> - ) - })} - </div> - ) - } -) -ChartLegendContent.displayName = "ChartLegend" - -// Helper to extract item config from a payload. -function getPayloadConfigFromPayload( - config: ChartConfig, - payload: unknown, - key: string -) { - if (typeof payload !== "object" || payload === null) { - return undefined - } - - const payloadPayload = - "payload" in payload && - typeof payload.payload === "object" && - payload.payload !== null - ? payload.payload - : undefined - - let configLabelKey: string = key - - if ( - key in payload && - typeof payload[key as keyof typeof payload] === "string" - ) { - configLabelKey = payload[key as keyof typeof payload] as string - } else if ( - payloadPayload && - key in payloadPayload && - typeof payloadPayload[key as keyof typeof payloadPayload] === "string" - ) { - configLabelKey = payloadPayload[ - key as keyof typeof payloadPayload - ] as string - } - - return configLabelKey in config - ? config[configLabelKey] - : config[key as keyof typeof config] -} - -export { - ChartContainer, - ChartTooltip, - ChartTooltipContent, - ChartLegend, - ChartLegendContent, - ChartStyle, -} diff --git a/ui/src/components/ui/data-table.tsx b/ui/src/components/ui/data-table.tsx deleted file mode 100644 index cf96b620..00000000 --- a/ui/src/components/ui/data-table.tsx +++ /dev/null @@ -1,80 +0,0 @@ -"use client"; - -import { - ColumnDef, - flexRender, - getCoreRowModel, - useReactTable, -} from "@tanstack/react-table"; - -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; - -interface DataTableProps<TData, TValue> { - columns: ColumnDef<TData, TValue>[]; - data: TData[]; -} - -export default function DataTable<TData, TValue>({ - columns, - data, -}: DataTableProps<TData, TValue>) { - const table = useReactTable({ - data, - columns, - getCoreRowModel: getCoreRowModel(), - }); - - return ( - <div className="rounded-md border"> - <Table> - <TableHeader> - {table.getHeaderGroups().map((headerGroup) => ( - <TableRow key={headerGroup.id}> - {headerGroup.headers.map((header) => { - return ( - <TableHead key={header.id}> - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - </TableHead> - ); - })} - </TableRow> - ))} - </TableHeader> - <TableBody> - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - <TableRow - key={row.id} - data-state={row.getIsSelected() && "selected"} - > - {row.getVisibleCells().map((cell) => ( - <TableCell key={cell.id}> - {flexRender(cell.column.columnDef.cell, cell.getContext())} - </TableCell> - ))} - </TableRow> - )) - ) : ( - <TableRow> - <TableCell colSpan={columns.length} className="h-24 text-center"> - No results. - </TableCell> - </TableRow> - )} - </TableBody> - </Table> - </div> - ); -} diff --git a/ui/src/components/ui/dialog.tsx b/ui/src/components/ui/dialog.tsx deleted file mode 100644 index c23630eb..00000000 --- a/ui/src/components/ui/dialog.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { X } from "lucide-react" - -import { cn } from "@/lib/utils" - -const Dialog = DialogPrimitive.Root - -const DialogTrigger = DialogPrimitive.Trigger - -const DialogPortal = DialogPrimitive.Portal - -const DialogClose = DialogPrimitive.Close - -const DialogOverlay = React.forwardRef< - React.ElementRef<typeof DialogPrimitive.Overlay>, - React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> ->(({ className, ...props }, ref) => ( - <DialogPrimitive.Overlay - ref={ref} - className={cn( - "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", - className - )} - {...props} - /> -)) -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName - -const DialogContent = React.forwardRef< - React.ElementRef<typeof DialogPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> ->(({ className, children, ...props }, ref) => ( - <DialogPortal> - <DialogOverlay /> - <DialogPrimitive.Content - ref={ref} - className={cn( - "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", - className - )} - {...props} - > - {children} - <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> - <X className="h-4 w-4" /> - <span className="sr-only">Close</span> - </DialogPrimitive.Close> - </DialogPrimitive.Content> - </DialogPortal> -)) -DialogContent.displayName = DialogPrimitive.Content.displayName - -const DialogHeader = ({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn( - "flex flex-col space-y-1.5 text-center sm:text-left", - className - )} - {...props} - /> -) -DialogHeader.displayName = "DialogHeader" - -const DialogFooter = ({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn( - "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", - className - )} - {...props} - /> -) -DialogFooter.displayName = "DialogFooter" - -const DialogTitle = React.forwardRef< - React.ElementRef<typeof DialogPrimitive.Title>, - React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> ->(({ className, ...props }, ref) => ( - <DialogPrimitive.Title - ref={ref} - className={cn( - "text-lg font-semibold leading-none tracking-tight", - className - )} - {...props} - /> -)) -DialogTitle.displayName = DialogPrimitive.Title.displayName - -const DialogDescription = React.forwardRef< - React.ElementRef<typeof DialogPrimitive.Description>, - React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> ->(({ className, ...props }, ref) => ( - <DialogPrimitive.Description - ref={ref} - className={cn("text-sm text-muted-foreground", className)} - {...props} - /> -)) -DialogDescription.displayName = DialogPrimitive.Description.displayName - -export { - Dialog, - DialogPortal, - DialogOverlay, - DialogClose, - DialogTrigger, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, -} diff --git a/ui/src/components/ui/dropdown-menu.tsx b/ui/src/components/ui/dropdown-menu.tsx deleted file mode 100644 index 769ff7aa..00000000 --- a/ui/src/components/ui/dropdown-menu.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import * as React from "react" -import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" -import { Check, ChevronRight, Circle } from "lucide-react" - -import { cn } from "@/lib/utils" - -const DropdownMenu = DropdownMenuPrimitive.Root - -const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger - -const DropdownMenuGroup = DropdownMenuPrimitive.Group - -const DropdownMenuPortal = DropdownMenuPrimitive.Portal - -const DropdownMenuSub = DropdownMenuPrimitive.Sub - -const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup - -const DropdownMenuSubTrigger = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { - inset?: boolean - } ->(({ className, inset, children, ...props }, ref) => ( - <DropdownMenuPrimitive.SubTrigger - ref={ref} - className={cn( - "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent", - inset && "pl-8", - className - )} - {...props} - > - {children} - <ChevronRight className="ml-auto h-4 w-4" /> - </DropdownMenuPrimitive.SubTrigger> -)) -DropdownMenuSubTrigger.displayName = - DropdownMenuPrimitive.SubTrigger.displayName - -const DropdownMenuSubContent = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent> ->(({ className, ...props }, ref) => ( - <DropdownMenuPrimitive.SubContent - ref={ref} - className={cn( - "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", - className - )} - {...props} - /> -)) -DropdownMenuSubContent.displayName = - DropdownMenuPrimitive.SubContent.displayName - -const DropdownMenuContent = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> ->(({ className, sideOffset = 4, ...props }, ref) => ( - <DropdownMenuPrimitive.Portal> - <DropdownMenuPrimitive.Content - ref={ref} - sideOffset={sideOffset} - className={cn( - "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", - className - )} - {...props} - /> - </DropdownMenuPrimitive.Portal> -)) -DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName - -const DropdownMenuItem = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.Item>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { - inset?: boolean - } ->(({ className, inset, ...props }, ref) => ( - <DropdownMenuPrimitive.Item - ref={ref} - className={cn( - "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", - inset && "pl-8", - className - )} - {...props} - /> -)) -DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName - -const DropdownMenuCheckboxItem = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> ->(({ className, children, checked, ...props }, ref) => ( - <DropdownMenuPrimitive.CheckboxItem - ref={ref} - className={cn( - "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", - className - )} - checked={checked} - {...props} - > - <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> - <DropdownMenuPrimitive.ItemIndicator> - <Check className="h-4 w-4" /> - </DropdownMenuPrimitive.ItemIndicator> - </span> - {children} - </DropdownMenuPrimitive.CheckboxItem> -)) -DropdownMenuCheckboxItem.displayName = - DropdownMenuPrimitive.CheckboxItem.displayName - -const DropdownMenuRadioItem = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> ->(({ className, children, ...props }, ref) => ( - <DropdownMenuPrimitive.RadioItem - ref={ref} - className={cn( - "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", - className - )} - {...props} - > - <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> - <DropdownMenuPrimitive.ItemIndicator> - <Circle className="h-2 w-2 fill-current" /> - </DropdownMenuPrimitive.ItemIndicator> - </span> - {children} - </DropdownMenuPrimitive.RadioItem> -)) -DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName - -const DropdownMenuLabel = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.Label>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { - inset?: boolean - } ->(({ className, inset, ...props }, ref) => ( - <DropdownMenuPrimitive.Label - ref={ref} - className={cn( - "px-2 py-1.5 text-sm font-semibold", - inset && "pl-8", - className - )} - {...props} - /> -)) -DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName - -const DropdownMenuSeparator = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.Separator>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> ->(({ className, ...props }, ref) => ( - <DropdownMenuPrimitive.Separator - ref={ref} - className={cn("-mx-1 my-1 h-px bg-muted", className)} - {...props} - /> -)) -DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName - -const DropdownMenuShortcut = ({ - className, - ...props -}: React.HTMLAttributes<HTMLSpanElement>) => { - return ( - <span - className={cn("ml-auto text-xs tracking-widest opacity-60", className)} - {...props} - /> - ) -} -DropdownMenuShortcut.displayName = "DropdownMenuShortcut" - -export { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuCheckboxItem, - DropdownMenuRadioItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuGroup, - DropdownMenuPortal, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuRadioGroup, -} diff --git a/ui/src/components/ui/table.tsx b/ui/src/components/ui/table.tsx deleted file mode 100644 index 7f3502f8..00000000 --- a/ui/src/components/ui/table.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import * as React from "react" - -import { cn } from "@/lib/utils" - -const Table = React.forwardRef< - HTMLTableElement, - React.HTMLAttributes<HTMLTableElement> ->(({ className, ...props }, ref) => ( - <div className="relative w-full overflow-auto"> - <table - ref={ref} - className={cn("w-full caption-bottom text-sm", className)} - {...props} - /> - </div> -)) -Table.displayName = "Table" - -const TableHeader = React.forwardRef< - HTMLTableSectionElement, - React.HTMLAttributes<HTMLTableSectionElement> ->(({ className, ...props }, ref) => ( - <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} /> -)) -TableHeader.displayName = "TableHeader" - -const TableBody = React.forwardRef< - HTMLTableSectionElement, - React.HTMLAttributes<HTMLTableSectionElement> ->(({ className, ...props }, ref) => ( - <tbody - ref={ref} - className={cn("[&_tr:last-child]:border-0", className)} - {...props} - /> -)) -TableBody.displayName = "TableBody" - -const TableFooter = React.forwardRef< - HTMLTableSectionElement, - React.HTMLAttributes<HTMLTableSectionElement> ->(({ className, ...props }, ref) => ( - <tfoot - ref={ref} - className={cn( - "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", - className - )} - {...props} - /> -)) -TableFooter.displayName = "TableFooter" - -const TableRow = React.forwardRef< - HTMLTableRowElement, - React.HTMLAttributes<HTMLTableRowElement> ->(({ className, ...props }, ref) => ( - <tr - ref={ref} - className={cn( - "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", - className - )} - {...props} - /> -)) -TableRow.displayName = "TableRow" - -const TableHead = React.forwardRef< - HTMLTableCellElement, - React.ThHTMLAttributes<HTMLTableCellElement> ->(({ className, ...props }, ref) => ( - <th - ref={ref} - className={cn( - "h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0", - className - )} - {...props} - /> -)) -TableHead.displayName = "TableHead" - -const TableCell = React.forwardRef< - HTMLTableCellElement, - React.TdHTMLAttributes<HTMLTableCellElement> ->(({ className, ...props }, ref) => ( - <td - ref={ref} - className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} - {...props} - /> -)) -TableCell.displayName = "TableCell" - -const TableCaption = React.forwardRef< - HTMLTableCaptionElement, - React.HTMLAttributes<HTMLTableCaptionElement> ->(({ className, ...props }, ref) => ( - <caption - ref={ref} - className={cn("mt-4 text-sm text-muted-foreground", className)} - {...props} - /> -)) -TableCaption.displayName = "TableCaption" - -export { - Table, - TableHeader, - TableBody, - TableFooter, - TableHead, - TableRow, - TableCell, - TableCaption, -} diff --git a/ui/src/components/ui/toast.tsx b/ui/src/components/ui/toast.tsx deleted file mode 100644 index a8224775..00000000 --- a/ui/src/components/ui/toast.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import * as React from "react" -import * as ToastPrimitives from "@radix-ui/react-toast" -import { cva, type VariantProps } from "class-variance-authority" -import { X } from "lucide-react" - -import { cn } from "@/lib/utils" - -const ToastProvider = ToastPrimitives.Provider - -const ToastViewport = React.forwardRef< - React.ElementRef<typeof ToastPrimitives.Viewport>, - React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport> ->(({ className, ...props }, ref) => ( - <ToastPrimitives.Viewport - ref={ref} - className={cn( - "fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]", - className - )} - {...props} - /> -)) -ToastViewport.displayName = ToastPrimitives.Viewport.displayName - -const toastVariants = cva( - "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", - { - variants: { - variant: { - default: "border bg-background text-foreground", - destructive: - "destructive group border-destructive bg-destructive text-destructive-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -const Toast = React.forwardRef< - React.ElementRef<typeof ToastPrimitives.Root>, - React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & - VariantProps<typeof toastVariants> ->(({ className, variant, ...props }, ref) => { - return ( - <ToastPrimitives.Root - ref={ref} - className={cn(toastVariants({ variant }), className)} - {...props} - /> - ) -}) -Toast.displayName = ToastPrimitives.Root.displayName - -const ToastAction = React.forwardRef< - React.ElementRef<typeof ToastPrimitives.Action>, - React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action> ->(({ className, ...props }, ref) => ( - <ToastPrimitives.Action - ref={ref} - className={cn( - "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive", - className - )} - {...props} - /> -)) -ToastAction.displayName = ToastPrimitives.Action.displayName - -const ToastClose = React.forwardRef< - React.ElementRef<typeof ToastPrimitives.Close>, - React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close> ->(({ className, ...props }, ref) => ( - <ToastPrimitives.Close - ref={ref} - className={cn( - "absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600", - className - )} - toast-close="" - {...props} - > - <X className="h-4 w-4" /> - </ToastPrimitives.Close> -)) -ToastClose.displayName = ToastPrimitives.Close.displayName - -const ToastTitle = React.forwardRef< - React.ElementRef<typeof ToastPrimitives.Title>, - React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title> ->(({ className, ...props }, ref) => ( - <ToastPrimitives.Title - ref={ref} - className={cn("text-sm font-semibold", className)} - {...props} - /> -)) -ToastTitle.displayName = ToastPrimitives.Title.displayName - -const ToastDescription = React.forwardRef< - React.ElementRef<typeof ToastPrimitives.Description>, - React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description> ->(({ className, ...props }, ref) => ( - <ToastPrimitives.Description - ref={ref} - className={cn("text-sm opacity-90", className)} - {...props} - /> -)) -ToastDescription.displayName = ToastPrimitives.Description.displayName - -type ToastProps = React.ComponentPropsWithoutRef<typeof Toast> - -type ToastActionElement = React.ReactElement<typeof ToastAction> - -export { - type ToastProps, - type ToastActionElement, - ToastProvider, - ToastViewport, - Toast, - ToastTitle, - ToastDescription, - ToastClose, - ToastAction, -} diff --git a/ui/src/components/ui/toaster.tsx b/ui/src/components/ui/toaster.tsx deleted file mode 100644 index a2209ba5..00000000 --- a/ui/src/components/ui/toaster.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { - Toast, - ToastClose, - ToastDescription, - ToastProvider, - ToastTitle, - ToastViewport, -} from "@/components/ui/toast" -import { useToast } from "@/components/ui/use-toast" - -export function Toaster() { - const { toasts } = useToast() - - return ( - <ToastProvider> - {toasts.map(function ({ id, title, description, action, ...props }) { - return ( - <Toast key={id} {...props}> - <div className="grid gap-1"> - {title && <ToastTitle>{title}</ToastTitle>} - {description && ( - <ToastDescription>{description}</ToastDescription> - )} - </div> - {action} - <ToastClose /> - </Toast> - ) - })} - <ToastViewport /> - </ToastProvider> - ) -} diff --git a/ui/src/components/ui/use-toast.ts b/ui/src/components/ui/use-toast.ts deleted file mode 100644 index 16713070..00000000 --- a/ui/src/components/ui/use-toast.ts +++ /dev/null @@ -1,192 +0,0 @@ -// Inspired by react-hot-toast library -import * as React from "react" - -import type { - ToastActionElement, - ToastProps, -} from "@/components/ui/toast" - -const TOAST_LIMIT = 1 -const TOAST_REMOVE_DELAY = 1000000 - -type ToasterToast = ToastProps & { - id: string - title?: React.ReactNode - description?: React.ReactNode - action?: ToastActionElement -} - -const actionTypes = { - ADD_TOAST: "ADD_TOAST", - UPDATE_TOAST: "UPDATE_TOAST", - DISMISS_TOAST: "DISMISS_TOAST", - REMOVE_TOAST: "REMOVE_TOAST", -} as const - -let count = 0 - -function genId() { - count = (count + 1) % Number.MAX_SAFE_INTEGER - return count.toString() -} - -type ActionType = typeof actionTypes - -type Action = - | { - type: ActionType["ADD_TOAST"] - toast: ToasterToast - } - | { - type: ActionType["UPDATE_TOAST"] - toast: Partial<ToasterToast> - } - | { - type: ActionType["DISMISS_TOAST"] - toastId?: ToasterToast["id"] - } - | { - type: ActionType["REMOVE_TOAST"] - toastId?: ToasterToast["id"] - } - -interface State { - toasts: ToasterToast[] -} - -const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>() - -const addToRemoveQueue = (toastId: string) => { - if (toastTimeouts.has(toastId)) { - return - } - - const timeout = setTimeout(() => { - toastTimeouts.delete(toastId) - dispatch({ - type: "REMOVE_TOAST", - toastId: toastId, - }) - }, TOAST_REMOVE_DELAY) - - toastTimeouts.set(toastId, timeout) -} - -export const reducer = (state: State, action: Action): State => { - switch (action.type) { - case "ADD_TOAST": - return { - ...state, - toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), - } - - case "UPDATE_TOAST": - return { - ...state, - toasts: state.toasts.map((t) => - t.id === action.toast.id ? { ...t, ...action.toast } : t - ), - } - - case "DISMISS_TOAST": { - const { toastId } = action - - // ! Side effects ! - This could be extracted into a dismissToast() action, - // but I'll keep it here for simplicity - if (toastId) { - addToRemoveQueue(toastId) - } else { - state.toasts.forEach((toast) => { - addToRemoveQueue(toast.id) - }) - } - - return { - ...state, - toasts: state.toasts.map((t) => - t.id === toastId || toastId === undefined - ? { - ...t, - open: false, - } - : t - ), - } - } - case "REMOVE_TOAST": - if (action.toastId === undefined) { - return { - ...state, - toasts: [], - } - } - return { - ...state, - toasts: state.toasts.filter((t) => t.id !== action.toastId), - } - } -} - -const listeners: Array<(state: State) => void> = [] - -let memoryState: State = { toasts: [] } - -function dispatch(action: Action) { - memoryState = reducer(memoryState, action) - listeners.forEach((listener) => { - listener(memoryState) - }) -} - -type Toast = Omit<ToasterToast, "id"> - -function toast({ ...props }: Toast) { - const id = genId() - - const update = (props: ToasterToast) => - dispatch({ - type: "UPDATE_TOAST", - toast: { ...props, id }, - }) - const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) - - dispatch({ - type: "ADD_TOAST", - toast: { - ...props, - id, - open: true, - onOpenChange: (open) => { - if (!open) dismiss() - }, - }, - }) - - return { - id: id, - dismiss, - update, - } -} - -function useToast() { - const [state, setState] = React.useState<State>(memoryState) - - React.useEffect(() => { - listeners.push(setState) - return () => { - const index = listeners.indexOf(setState) - if (index > -1) { - listeners.splice(index, 1) - } - } - }, [state]) - - return { - ...state, - toast, - dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), - } -} - -export { useToast, toast } |
