use Tooltip component instead of HoverCard for mobile compatibility

This commit is contained in:
mbecker20
2025-03-13 00:01:26 -04:00
parent 93ccc1ce7f
commit f022e83414
8 changed files with 253 additions and 83 deletions

View File

@@ -19,7 +19,6 @@ import { ResourceComponents } from "..";
import { Types } from "komodo_client";
import { DashboardPieChart } from "@pages/home/dashboard";
import { ResourcePageHeader, StatusBadge } from "@components/util";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@ui/hover-card";
import { Card } from "@ui/card";
import { Badge } from "@ui/badge";
import { useToast } from "@ui/use-toast";
@@ -27,6 +26,7 @@ import { Button } from "@ui/button";
import { useBuilder } from "../builder";
import { RenameResource } from "@components/config/util";
import { GroupActions } from "@components/group-actions";
import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/tooltip";
export const useBuild = (id?: string) =>
useRead("ListBuilds", {}, { refetchInterval: 10_000 }).data?.find(
@@ -153,8 +153,8 @@ export const BuildComponents: RequiredResourceComponents = {
const out_of_date =
info.built_hash && info.built_hash !== info.latest_hash;
return (
<HoverCard openDelay={200}>
<HoverCardTrigger asChild>
<Tooltip>
<TooltipTrigger asChild>
<Card
className={cn(
"px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer",
@@ -166,8 +166,8 @@ export const BuildComponents: RequiredResourceComponents = {
{info.built_hash || info.latest_hash}
</div>
</Card>
</HoverCardTrigger>
<HoverCardContent align="start">
</TooltipTrigger>
<TooltipContent>
<div className="grid gap-2">
<Badge
variant="secondary"
@@ -196,8 +196,8 @@ export const BuildComponents: RequiredResourceComponents = {
</>
)}
</div>
</HoverCardContent>
</HoverCard>
</TooltipContent>
</Tooltip>
);
},
Refresh: ({ id }) => {

View File

@@ -29,8 +29,8 @@ import {
StatusBadge,
} from "@components/util";
import { RenameResource } from "@components/config/util";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@ui/hover-card";
import { GroupActions } from "@components/group-actions";
import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/tooltip";
// const configOrLog = atomWithStorage("config-or-log-v1", "Config");
@@ -319,8 +319,8 @@ export const UpdateAvailable = ({
return null;
}
return (
<HoverCard openDelay={200}>
<HoverCardTrigger asChild>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
"px-2 py-1 border rounded-md border-blue-400 hover:border-blue-500 opacity-50 hover:opacity-70 transition-colors cursor-pointer flex items-center gap-2",
@@ -334,10 +334,10 @@ export const UpdateAvailable = ({
</div>
)}
</div>
</HoverCardTrigger>
<HoverCardContent align="start" className="w-fit text-sm">
</TooltipTrigger>
<TooltipContent className="w-fit text-sm">
There is a newer image available
</HoverCardContent>
</HoverCard>
</TooltipContent>
</Tooltip>
);
};

View File

@@ -48,7 +48,6 @@ import {
} from "@ui/command";
import { Switch } from "@ui/switch";
import { DataTable } from "@ui/data-table";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@ui/hover-card";
import {
DropdownMenu,
DropdownMenuContent,
@@ -61,6 +60,7 @@ import { filterBySplit } from "@lib/utils";
import { useToast } from "@ui/use-toast";
import { fmt_upper_camelcase } from "@lib/formatting";
import { TextUpdateMenuMonaco } from "@components/util";
import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/tooltip";
export const ProcedureConfig = ({ id }: { id: string }) => {
const procedure = useRead("GetProcedure", { procedure: id }).data;
@@ -109,19 +109,19 @@ const ProcedureConfigInner = ({
<Settings className="w-4 h-4" />
<h2 className="text-xl">Config</h2>
</div>
<HoverCard openDelay={200}>
<HoverCardTrigger asChild>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline">
<Info className="w-4 h-4" />
</Button>
</HoverCardTrigger>
<HoverCardContent align="start">
</TooltipTrigger>
<TooltipContent>
<div>
The executions in a stage are all run in parallel. The stages
themselves are run sequentially.
</div>
</HoverCardContent>
</HoverCard>
</TooltipContent>
</Tooltip>
</div>
}
disabled={disabled}

View File

@@ -11,7 +11,6 @@ import {
stroke_color_class_by_intention,
} from "@lib/color";
import { cn } from "@lib/utils";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@ui/hover-card";
import { useServer } from "../server";
import { Types } from "komodo_client";
import { DashboardPieChart } from "@pages/home/dashboard";
@@ -22,6 +21,7 @@ import { Button } from "@ui/button";
import { useBuilder } from "../builder";
import { RenameResource } from "@components/config/util";
import { GroupActions } from "@components/group-actions";
import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/tooltip";
export const useRepo = (id?: string) =>
useRead("ListRepos", {}, { refetchInterval: 10_000 }).data?.find(
@@ -97,21 +97,21 @@ export const RepoComponents: RequiredResourceComponents = {
return null;
}
return (
<HoverCard openDelay={200}>
<HoverCardTrigger asChild>
<Tooltip>
<TooltipTrigger asChild>
<Card className="px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer">
<div className="text-muted-foreground text-sm text-nowrap overflow-hidden overflow-ellipsis">
cloned: {info.cloned_hash}
</div>
</Card>
</HoverCardTrigger>
<HoverCardContent align="start">
</TooltipTrigger>
<TooltipContent>
<div className="grid">
<div className="text-muted-foreground">commit message:</div>
{info.cloned_message}
</div>
</HoverCardContent>
</HoverCard>
</TooltipContent>
</Tooltip>
);
},
Built: ({ id }) => {
@@ -121,15 +121,15 @@ export const RepoComponents: RequiredResourceComponents = {
return null;
}
return (
<HoverCard openDelay={200}>
<HoverCardTrigger asChild>
<Tooltip>
<TooltipTrigger asChild>
<Card className="px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer">
<div className="text-muted-foreground text-sm text-nowrap overflow-hidden overflow-ellipsis">
built: {info.built_hash}
</div>
</Card>
</HoverCardTrigger>
<HoverCardContent align="start">
</TooltipTrigger>
<TooltipContent>
<div className="grid gap-2">
<Badge
variant="secondary"
@@ -139,8 +139,8 @@ export const RepoComponents: RequiredResourceComponents = {
</Badge>
{fullInfo?.built_message}
</div>
</HoverCardContent>
</HoverCard>
</TooltipContent>
</Tooltip>
);
},
Latest: ({ id }) => {
@@ -150,15 +150,15 @@ export const RepoComponents: RequiredResourceComponents = {
return null;
}
return (
<HoverCard openDelay={200}>
<HoverCardTrigger asChild>
<Tooltip>
<TooltipTrigger asChild>
<Card className="px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer">
<div className="text-muted-foreground text-sm text-nowrap overflow-hidden overflow-ellipsis">
latest: {info.latest_hash}
</div>
</Card>
</HoverCardTrigger>
<HoverCardContent align="start">
</TooltipTrigger>
<TooltipContent>
<div className="grid gap-2">
<Badge
variant="secondary"
@@ -168,8 +168,8 @@ export const RepoComponents: RequiredResourceComponents = {
</Badge>
{fullInfo?.latest_message}
</div>
</HoverCardContent>
</HoverCard>
</TooltipContent>
</Tooltip>
);
},
Refresh: ({ id }) => {

View File

@@ -12,7 +12,6 @@ import {
stroke_color_class_by_intention,
} from "@lib/color";
import { cn, sync_no_changes } from "@lib/utils";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@ui/hover-card";
import { fmt_date } from "@lib/formatting";
import { DashboardPieChart } from "@pages/home/dashboard";
import { ResourcePageHeader, StatusBadge } from "@components/util";
@@ -24,6 +23,7 @@ import { Badge } from "@ui/badge";
import { RenameResource } from "@components/config/util";
import { GroupActions } from "@components/group-actions";
import { useAtom } from "jotai";
import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/tooltip";
export const useResourceSync = (id?: string) =>
useRead("ListResourceSyncs", {}, { refetchInterval: 10_000 }).data?.find(
@@ -184,8 +184,8 @@ export const ResourceSyncComponents: RequiredResourceComponents = {
const out_of_date =
info.last_sync_hash && info.last_sync_hash !== info.pending_hash;
return (
<HoverCard openDelay={200}>
<HoverCardTrigger asChild>
<Tooltip>
<TooltipTrigger asChild>
<Card
className={cn(
"px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer",
@@ -197,8 +197,8 @@ export const ResourceSyncComponents: RequiredResourceComponents = {
{info.last_sync_hash || info.pending_hash}
</div>
</Card>
</HoverCardTrigger>
<HoverCardContent align="start">
</TooltipTrigger>
<TooltipContent>
<div className="grid gap-2">
<Badge
variant="secondary"
@@ -227,8 +227,8 @@ export const ResourceSyncComponents: RequiredResourceComponents = {
</>
)}
</div>
</HoverCardContent>
</HoverCard>
</TooltipContent>
</Tooltip>
);
},
},

View File

@@ -18,7 +18,6 @@ import {
stroke_color_class_by_intention,
} from "@lib/color";
import { cn } from "@lib/utils";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@ui/hover-card";
import { useServer } from "../server";
import { Types } from "komodo_client";
import {
@@ -41,6 +40,7 @@ import { StackConfig } from "./config";
import { RenameResource } from "@components/config/util";
import { GroupActions } from "@components/group-actions";
import { StackLogs } from "./log";
import { Tooltip, TooltipTrigger, TooltipContent } from "@ui/tooltip";
export const useStack = (id?: string) =>
useRead("ListStacks", {}, { refetchInterval: 10_000 }).data?.find(
@@ -210,22 +210,22 @@ export const StackComponents: RequiredResourceComponents = {
return null;
}
return (
<HoverCard openDelay={200}>
<HoverCardTrigger asChild>
<Tooltip>
<TooltipTrigger asChild>
<Card className="px-3 py-2 bg-destructive/75 hover:bg-destructive transition-colors cursor-pointer">
<div className="text-sm text-nowrap overflow-hidden overflow-ellipsis">
Config Missing
</div>
</Card>
</HoverCardTrigger>
<HoverCardContent align="start">
</TooltipTrigger>
<TooltipContent>
<div className="grid gap-2">
No configuration provided for stack. Cannot get stack state.
Either paste the compose file contents into the UI, or configure a
git repo containing your files.
</div>
</HoverCardContent>
</HoverCard>
</TooltipContent>
</Tooltip>
);
},
ProjectMissing: ({ id }) => {
@@ -239,22 +239,22 @@ export const StackComponents: RequiredResourceComponents = {
return null;
}
return (
<HoverCard openDelay={200}>
<HoverCardTrigger asChild>
<Tooltip>
<TooltipTrigger asChild>
<Card className="px-3 py-2 bg-destructive/75 hover:bg-destructive transition-colors cursor-pointer">
<div className="text-sm text-nowrap overflow-hidden overflow-ellipsis">
Project Missing
</div>
</Card>
</HoverCardTrigger>
<HoverCardContent align="start">
</TooltipTrigger>
<TooltipContent>
<div className="grid gap-2">
The compose project is not on the host. If the compose stack is
running, the 'Project Name' needs to be set. This can be found
with 'docker compose ls'.
</div>
</HoverCardContent>
</HoverCard>
</TooltipContent>
</Tooltip>
);
},
RemoteErrors: ({ id }) => {
@@ -264,21 +264,21 @@ export const StackComponents: RequiredResourceComponents = {
return null;
}
return (
<HoverCard openDelay={200}>
<HoverCardTrigger asChild>
<Tooltip>
<TooltipTrigger asChild>
<Card className="px-3 py-2 bg-destructive/75 hover:bg-destructive transition-colors cursor-pointer">
<div className="text-sm text-nowrap overflow-hidden overflow-ellipsis">
Remote Error
</div>
</Card>
</HoverCardTrigger>
<HoverCardContent align="start">
</TooltipTrigger>
<TooltipContent>
<div>
There are errors reading the remote file contents. See{" "}
<span className="font-bold">Info</span> tab for details.
</div>
</HoverCardContent>
</HoverCard>
</TooltipContent>
</Tooltip>
);
},
UpdateAvailable: ({ id }) => <UpdateAvailable id={id} />,
@@ -301,8 +301,8 @@ export const StackComponents: RequiredResourceComponents = {
const out_of_date =
info.deployed_hash && info.deployed_hash !== info.latest_hash;
return (
<HoverCard openDelay={200}>
<HoverCardTrigger asChild>
<Tooltip>
<TooltipTrigger asChild>
<Card
className={cn(
"px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer",
@@ -314,8 +314,8 @@ export const StackComponents: RequiredResourceComponents = {
{info.deployed_hash || info.latest_hash}
</div>
</Card>
</HoverCardTrigger>
<HoverCardContent align="start">
</TooltipTrigger>
<TooltipContent>
<div className="grid gap-2">
<Badge
variant="secondary"
@@ -344,8 +344,8 @@ export const StackComponents: RequiredResourceComponents = {
</>
)}
</div>
</HoverCardContent>
</HoverCard>
</TooltipContent>
</Tooltip>
);
},
Refresh: ({ id }) => {
@@ -475,8 +475,8 @@ export const UpdateAvailable = ({
return null;
}
return (
<HoverCard openDelay={200}>
<HoverCardTrigger asChild>
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
"px-2 py-1 border rounded-md border-blue-400 hover:border-blue-500 opacity-50 hover:opacity-70 transition-colors cursor-pointer flex items-center gap-2",
@@ -495,8 +495,8 @@ export const UpdateAvailable = ({
</div>
)}
</div>
</HoverCardTrigger>
<HoverCardContent align="start" className="flex flex-col gap-2 w-fit">
</TooltipTrigger>
<TooltipContent className="flex flex-col gap-2 w-fit">
{info?.services
.filter((service) => service.update_available)
.map((s) => (
@@ -506,7 +506,7 @@ export const UpdateAvailable = ({
<div>{s.image}</div>
</div>
))}
</HoverCardContent>
</HoverCard>
</TooltipContent>
</Tooltip>
);
};

View File

@@ -32,11 +32,15 @@ export const Resources = () => {
const [search, set] = useState("");
const [filter_update_available, toggle_filter_update_available] =
useFilterByUpdateAvailable();
const resources = useRead(`List${type}s`, {
query: {
specific: { update_available: filter_update_available },
},
}).data;
const query =
type === "Stack" || type === "Deployment"
? {
query: {
specific: { update_available: filter_update_available },
},
}
: {};
const resources = useRead(`List${type}s`, query).data;
const filtered = useFilterResources(resources as any, search);
const Components = ResourceComponents[type];

166
frontend/src/ui/tooltip.tsx Normal file
View File

@@ -0,0 +1,166 @@
import * as React from "react";
import {
useFloating,
autoUpdate,
offset,
flip,
shift,
useHover,
useFocus,
useDismiss,
useRole,
useInteractions,
useMergeRefs,
FloatingPortal,
Placement,
} from "@floating-ui/react";
import { cn } from "@lib/utils";
interface TooltipOptions {
initialOpen?: boolean;
placement?: Placement;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
export function useTooltip({
initialOpen = false,
placement = "bottom-start",
open: controlledOpen,
onOpenChange: setControlledOpen,
}: TooltipOptions = {}) {
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(initialOpen);
const open = controlledOpen ?? uncontrolledOpen;
const setOpen = setControlledOpen ?? setUncontrolledOpen;
const data = useFloating({
placement,
open,
onOpenChange: setOpen,
whileElementsMounted: autoUpdate,
middleware: [
offset(5),
flip({
crossAxis: placement.includes("-"),
fallbackAxisSideDirection: "start",
padding: 5,
}),
shift({ padding: 5 }),
],
});
const context = data.context;
const hover = useHover(context, {
move: false,
enabled: controlledOpen == null,
});
const focus = useFocus(context, {
enabled: controlledOpen == null,
});
const dismiss = useDismiss(context);
const role = useRole(context, { role: "tooltip" });
const interactions = useInteractions([hover, focus, dismiss, role]);
return React.useMemo(
() => ({
open,
setOpen,
...interactions,
...data,
}),
[open, setOpen, interactions, data]
);
}
type ContextType = ReturnType<typeof useTooltip> | null;
const TooltipContext = React.createContext<ContextType>(null);
export const useTooltipContext = () => {
const context = React.useContext(TooltipContext);
if (context == null) {
throw new Error("Tooltip components must be wrapped in <Tooltip />");
}
return context;
};
export function Tooltip({
children,
...options
}: { children: React.ReactNode } & TooltipOptions) {
// This can accept any props as options, e.g. `placement`,
// or other positioning options.
const tooltip = useTooltip(options);
return (
<TooltipContext.Provider value={tooltip}>
{children}
</TooltipContext.Provider>
);
}
export const TooltipTrigger = React.forwardRef<
HTMLElement,
React.HTMLProps<HTMLElement> & { asChild?: boolean }
>(({ children, asChild = false, ...props }, propRef) => {
const context = useTooltipContext();
const childrenRef = (children as any).ref;
const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef]);
// `asChild` allows the user to pass any element as the anchor
if (asChild && React.isValidElement(children)) {
return React.cloneElement(
children,
context.getReferenceProps({
ref,
...props,
...(children.props as any),
"data-state": context.open ? "open" : "closed",
})
);
}
return (
<button
ref={ref}
// The user can style the trigger based on the state
data-state={context.open ? "open" : "closed"}
{...context.getReferenceProps(props)}
type="button"
>
{children}
</button>
);
});
TooltipTrigger.displayName = "TooltipTrigger";
export const TooltipContent = React.forwardRef<
HTMLDivElement,
React.HTMLProps<HTMLDivElement>
>(({ className, ...props }, propRef) => {
const context = useTooltipContext();
const ref = useMergeRefs([context.refs.setFloating, propRef]);
if (!context.open) return null;
return (
<FloatingPortal>
<div
ref={ref}
style={{
...context.floatingStyles,
}}
{...context.getFloatingProps(props)}
className={cn(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none",
className
)}
/>
</FloatingPortal>
);
});
TooltipContent.displayName = "TooltipContent";