diff options
| author | Ellie Huxtable <ellie@atuin.sh> | 2024-07-30 16:54:10 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-07-30 16:54:10 +0100 |
| commit | 808138de633e410c1d3867d4fb7cb74967647605 (patch) | |
| tree | f180b7066b91d8d8d8006219a118439be1621d74 /ui/src/components | |
| parent | chore(deps): bump debian (#2320) (diff) | |
| download | atuin-808138de633e410c1d3867d4fb7cb74967647605.zip | |
chore: remove ui directory (#2329)
This is still in development, but rather than clutter the commit history
and issues with an unreleased project I've split the UI into its own
repo.
Once ready for release, I'll either merge the ui code back in, or just
make the repo public.
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 } |
