mirror of
https://github.com/moghtech/komodo.git
synced 2026-09-10 00:01:02 +00:00
improve dashboard
This commit is contained in:
@@ -386,6 +386,7 @@ pub enum Operation {
|
||||
PruneNetworksServer,
|
||||
CreateNetwork,
|
||||
DeleteNetwork,
|
||||
StopAllContainers,
|
||||
|
||||
// build
|
||||
CreateBuild,
|
||||
@@ -405,7 +406,6 @@ pub enum Operation {
|
||||
DeleteDeployment,
|
||||
DeployContainer,
|
||||
StopContainer,
|
||||
StopAllContainers,
|
||||
StartContainer,
|
||||
RemoveContainer,
|
||||
RenameDeployment,
|
||||
|
||||
@@ -35,8 +35,6 @@ import { NewLayout } from "@components/layouts";
|
||||
import { Types } from "@monitor/client";
|
||||
import { ConfigItem, DoubleInput } from "@components/config/util";
|
||||
import { usableResourcePath } from "@lib/utils";
|
||||
import { Card } from "@ui/card";
|
||||
import { TagsWithBadge } from "@components/tags";
|
||||
|
||||
export const ResourceDescription = ({
|
||||
type,
|
||||
@@ -345,27 +343,3 @@ export const ServerSelector = ({
|
||||
/>
|
||||
</ConfigItem>
|
||||
);
|
||||
|
||||
export const RecentCard = ({
|
||||
type,
|
||||
id,
|
||||
}: {
|
||||
type: UsableResource;
|
||||
id: string;
|
||||
}) => {
|
||||
const Components = ResourceComponents[type];
|
||||
const tags = Components.list_item(id)?.tags;
|
||||
return (
|
||||
<Link to={`${usableResourcePath(type)}/${id}`} className="h-full">
|
||||
<Card className="h-full px-6 py-4 flex flex-col justify-between hover:bg-accent/50 transition-colors cursor-pointer">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<Components.Name id={id} />
|
||||
<Components.Icon id={id} />
|
||||
</div>
|
||||
<div className="flex items-end justify-end gap-2 w-full">
|
||||
<TagsWithBadge tag_ids={tags} />
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { fmt_date_with_minutes } from "@lib/formatting";
|
||||
import { Types } from "@monitor/client";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DataTable } from "@ui/data-table";
|
||||
import { useState } from "react";
|
||||
import { UpdateDetailsInner, UpdateUser } from "./details";
|
||||
import { bg_color_class_by_intention } from "@lib/color";
|
||||
import { Card, CardHeader } from "@ui/card";
|
||||
import { cn } from "@lib/utils";
|
||||
import { ResourceLink } from "@components/resources/common";
|
||||
|
||||
export const UpdatesTable = ({
|
||||
updates,
|
||||
@@ -12,46 +15,55 @@ export const UpdatesTable = ({
|
||||
updates: Types.UpdateListItem[];
|
||||
showTarget?: boolean;
|
||||
}) => {
|
||||
let data: ColumnDef<Types.UpdateListItem, string>[] = [
|
||||
{
|
||||
header: "Operation",
|
||||
accessorKey: "operation",
|
||||
},
|
||||
{
|
||||
header: "Status",
|
||||
accessorKey: "status",
|
||||
},
|
||||
{
|
||||
header: "Success",
|
||||
accessorFn: ({ success }) => (success ? "Ok" : "Fail"),
|
||||
},
|
||||
{
|
||||
header: "Start Time",
|
||||
accessorFn: ({ start_ts }) => fmt_date_with_minutes(new Date(start_ts)),
|
||||
},
|
||||
{
|
||||
header: "Operator",
|
||||
accessorKey: "operator",
|
||||
cell: ({ row }) => <UpdateUser user_id={row.original.operator} />,
|
||||
},
|
||||
];
|
||||
// attach the target column on front
|
||||
if (showTarget) {
|
||||
data = [
|
||||
{
|
||||
header: "Target",
|
||||
accessorKey: "target.id",
|
||||
},
|
||||
...data,
|
||||
];
|
||||
}
|
||||
const [id, setId] = useState("");
|
||||
return (
|
||||
<>
|
||||
<DataTable
|
||||
tableKey="updates"
|
||||
data={updates}
|
||||
columns={data}
|
||||
columns={[
|
||||
{
|
||||
header: "Operation",
|
||||
accessorKey: "operation",
|
||||
},
|
||||
showTarget && {
|
||||
header: "Target",
|
||||
cell: ({ row }) =>
|
||||
row.original.target.type === "System" ? (
|
||||
"N/A"
|
||||
) : (
|
||||
<ResourceLink
|
||||
type={row.original.target.type}
|
||||
id={row.original.target.id}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Status",
|
||||
cell: ({ row }) => {
|
||||
const color = bg_color_class_by_intention(
|
||||
row.original.success ? "Good" : "Critical"
|
||||
);
|
||||
return (
|
||||
<Card className={cn("w-fit", color)}>
|
||||
<CardHeader className="py-0 px-2">
|
||||
{row.original.success ? "Success" : "Fail"}
|
||||
</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Start Time",
|
||||
accessorFn: ({ start_ts }) =>
|
||||
fmt_date_with_minutes(new Date(start_ts)),
|
||||
},
|
||||
{
|
||||
header: "Operator",
|
||||
accessorKey: "operator",
|
||||
cell: ({ row }) => <UpdateUser user_id={row.original.operator} />,
|
||||
},
|
||||
]}
|
||||
onRowClick={(row) => setId(row.id)}
|
||||
/>
|
||||
<UpdateDetailsInner id={id} open={!!id} setOpen={() => setId("")} />
|
||||
|
||||
@@ -1,130 +1,84 @@
|
||||
import { Page, Section } from "@components/layouts";
|
||||
import { Box, Key, Tag } from "lucide-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@ui/card";
|
||||
import { ResourceComponents } from "@components/resources";
|
||||
import { OpenAlerts } from "@components/alert";
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { Page } from "@components/layouts";
|
||||
import { ResourceComponents } from "@components/resources";
|
||||
import { TagsWithBadge } from "@components/tags";
|
||||
import { AllUpdates } from "@components/updates/resource";
|
||||
import { useRead, useUser } from "@lib/hooks";
|
||||
import { usableResourcePath } from "@lib/utils";
|
||||
import { UsableResource } from "@types";
|
||||
import { Card } from "@ui/card";
|
||||
import { Separator } from "@ui/separator";
|
||||
import { History } from "lucide-react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export const Dashboard = () => {
|
||||
return (
|
||||
<Page title="">
|
||||
<OpenAlerts />
|
||||
<AllUpdates />
|
||||
{/* <RecentlyViewed /> */}
|
||||
<Resources />
|
||||
|
||||
<ResourceRow type="Deployment" />
|
||||
<ResourceRow type="Build" />
|
||||
<ResourceRow type="Repo" />
|
||||
<ResourceRow type="Server" />
|
||||
<ResourceRow type="Procedure" />
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
const Resources = () => {
|
||||
const ResourceRow = ({ type }: { type: UsableResource }) => {
|
||||
const recents = useUser().data?.[`recent_${type.toLowerCase()}s`]?.slice(
|
||||
0,
|
||||
6
|
||||
) as string[] | undefined;
|
||||
const resources = useRead(`List${type}s`, {})
|
||||
.data?.filter((r) => !recents?.includes(r.id))
|
||||
.map((r) => r.id);
|
||||
const ids = [
|
||||
...(recents ?? []),
|
||||
...(resources?.slice(0, 6 - (recents?.length || 0)) ?? []),
|
||||
];
|
||||
if (ids.length === 0) return;
|
||||
const Components = ResourceComponents[type];
|
||||
return (
|
||||
<Section title="Resources" icon={<Box className="w-4 h-4" />} actions="">
|
||||
<div className="flex flex-col lg:flex-row gap-4 w-full">
|
||||
<div className="flex flex-col md:flex-row gap-4 w-full">
|
||||
<ResourceComponents.Server.Dashboard />
|
||||
<ResourceComponents.Deployment.Dashboard />
|
||||
<div className="flex gap-4">
|
||||
<Components.Dashboard />
|
||||
<div className="py-2">
|
||||
<Separator orientation="vertical" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 w-full pb-1">
|
||||
<div className="flex gap-2 items-center text-muted-foreground">
|
||||
<History className="w-4 h-4" />
|
||||
<h3>Recent {type}s</h3>
|
||||
</div>
|
||||
<div className="w-full lg:max-w-[50%]">
|
||||
<ResourceComponents.Build.Dashboard />
|
||||
<div className="grid grid-cols-3 grid-rows-2 gap-4 w-full h-full">
|
||||
{ids.map((id: string) => (
|
||||
<RecentCard key={type + id} type={type} id={id} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col lg:flex-row gap-4 w-full">
|
||||
<ResourceComponents.Procedure.Dashboard />
|
||||
<ResourceComponents.Repo.Dashboard />
|
||||
</div>
|
||||
<div className="flex flex-col lg:flex-row gap-4 w-full">
|
||||
<div className="flex flex-col md:flex-row gap-4 w-full">
|
||||
<ResourceComponents.Alerter.Dashboard />
|
||||
<ResourceComponents.Builder.Dashboard />
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row gap-4 w-full">
|
||||
<TagsSummary />
|
||||
<ApiKeysSummary />
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// const RecentlyViewed = () => {
|
||||
// const nav = useNavigate();
|
||||
// const recently_viewed = useUser().data?.recently_viewed;
|
||||
// const checkResourceExists = useCheckResourceExists();
|
||||
// return (
|
||||
// <Section
|
||||
// title="Recently Viewed"
|
||||
// icon={<History className="w-4 h-4" />}
|
||||
// actions=""
|
||||
// >
|
||||
// <div className="grid md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-4">
|
||||
// {recently_viewed
|
||||
// ?.filter(checkResourceExists)
|
||||
// .slice(0, 8)
|
||||
// .map(({ type, id }, i) => (
|
||||
// <Fragment key={type + id}>
|
||||
// {type !== "System" && (
|
||||
// <Card
|
||||
// onClick={() => nav(`/${usableResourcePath(type)}/${id}`)}
|
||||
// className={cn(
|
||||
// "px-3 py-2 h-fit hover:bg-accent/50 group-focus:bg-accent/50 transition-colors cursor-pointer",
|
||||
// i > 5 && "hidden 2xl:flex"
|
||||
// )}
|
||||
// >
|
||||
// <CardContent className="flex items-center justify-between gap-4 px-3 py-2 text-sm text-muted-foreground w-full">
|
||||
// <ResourceLink type={type} id={id} />
|
||||
// {type}
|
||||
// </CardContent>
|
||||
// </Card>
|
||||
// )}
|
||||
// </Fragment>
|
||||
// ))}
|
||||
// </div>
|
||||
// </Section>
|
||||
// );
|
||||
// };
|
||||
|
||||
const TagsSummary = () => {
|
||||
const tags_count = useRead("ListTags", {}).data?.length;
|
||||
|
||||
const RecentCard = ({ type, id }: { type: UsableResource; id: string }) => {
|
||||
const Components = ResourceComponents[type];
|
||||
const tags = Components.list_item(id)?.tags;
|
||||
return (
|
||||
<Link to="/tags" className="w-full">
|
||||
<Card className="hover:bg-accent/50 transition-colors cursor-pointer">
|
||||
<CardHeader>
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
<CardTitle>Tags</CardTitle>
|
||||
<CardDescription>{tags_count} Total</CardDescription>
|
||||
</div>
|
||||
<Tag className="w-4 h-4" />
|
||||
<Link to={`${usableResourcePath(type)}/${id}`} className="h-full">
|
||||
<Card className="h-full px-6 py-4 flex flex-col justify-between hover:bg-accent/50 transition-colors cursor-pointer">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<Components.Icon id={id} />
|
||||
<Components.Name id={id} />
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const ApiKeysSummary = () => {
|
||||
const keys_count = useRead("ListApiKeys", {}).data?.length;
|
||||
|
||||
return (
|
||||
<Link to="/keys" className="w-full">
|
||||
<Card className="hover:bg-accent/50 transition-colors cursor-pointer">
|
||||
<CardHeader>
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
<CardTitle>Api Keys</CardTitle>
|
||||
<CardDescription>{keys_count} Total</CardDescription>
|
||||
</div>
|
||||
<Key className="w-4 h-4" />
|
||||
<div className="text-sm">
|
||||
<Components.Status.State id={id} />
|
||||
</div>
|
||||
</CardHeader>
|
||||
</div>
|
||||
<div className="flex items-end justify-end gap-2 w-full">
|
||||
<TagsWithBadge tag_ids={tags} />
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import { OpenAlerts } from "@components/alert";
|
||||
import { Page } from "@components/layouts";
|
||||
import { ResourceComponents } from "@components/resources";
|
||||
import { RecentCard } from "@components/resources/common";
|
||||
import { AllUpdates } from "@components/updates/resource";
|
||||
import { useRead, useUser } from "@lib/hooks";
|
||||
import { UsableResource } from "@types";
|
||||
import { Separator } from "@ui/separator";
|
||||
import { History } from "lucide-react";
|
||||
|
||||
export const Dashboard = () => {
|
||||
return (
|
||||
<Page title="">
|
||||
<OpenAlerts />
|
||||
<AllUpdates />
|
||||
|
||||
<ResourceRow type="Deployment" />
|
||||
<ResourceRow type="Build" />
|
||||
<ResourceRow type="Repo" />
|
||||
<ResourceRow type="Server" />
|
||||
<ResourceRow type="Procedure" />
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
const ResourceRow = ({ type }: { type: UsableResource }) => {
|
||||
const recents = useUser().data?.[`recent_${type.toLowerCase()}s`]?.slice(
|
||||
0,
|
||||
6
|
||||
) as string[] | undefined;
|
||||
const resources = useRead(`List${type}s`, {})
|
||||
.data?.filter((r) => !recents?.includes(r.id))
|
||||
.map((r) => r.id);
|
||||
const ids = [
|
||||
...(recents ?? []),
|
||||
...(resources?.slice(0, 6 - (recents?.length || 0)) ?? []),
|
||||
];
|
||||
const Components = ResourceComponents[type];
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
<Components.Dashboard />
|
||||
<div className="py-2">
|
||||
<Separator orientation="vertical" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 w-full pb-1">
|
||||
<div className="flex gap-2 items-center text-muted-foreground">
|
||||
<History className="w-4 h-4" />
|
||||
<h3>Recent {type}s</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 grid-rows-2 gap-4 w-full h-full">
|
||||
{ids.map((id: string) => (
|
||||
<RecentCard key={type + id} type={type} id={id} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { homeViewAtom } from "@main";
|
||||
import { useAtom } from "jotai";
|
||||
import { Dashboard } from "./dashboard2";
|
||||
import { Dashboard } from "./dashboard";
|
||||
import { AllResources } from "./all_resources";
|
||||
import { Tree } from "./tree";
|
||||
import { useSetTitle } from "@lib/hooks";
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import { Page } from "@components/layouts";
|
||||
import { ResourceComponents } from "@components/resources";
|
||||
import { AddTags, ResourceTags } from "@components/tags";
|
||||
import { UpdatesTable } from "@components/updates/table";
|
||||
import { useRead, useResourceParamType, useSetTitle } from "@lib/hooks";
|
||||
import { Types } from "@monitor/client";
|
||||
import { CaretSortIcon } from "@radix-ui/react-icons";
|
||||
import { UsableResource } from "@types";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@ui/command";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover";
|
||||
import { SearchX } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
export const Updates = () => {
|
||||
@@ -18,10 +30,16 @@ export const Updates = () => {
|
||||
|
||||
const AllUpdates = () => {
|
||||
useSetTitle("Updates");
|
||||
const updates = useRead("ListUpdates", {}).data;
|
||||
const [operation, setOperation] = useState<Types.Operation | undefined>();
|
||||
const updates = useRead("ListUpdates", { query: { operation } }).data;
|
||||
return (
|
||||
<Page title="Updates">
|
||||
<UpdatesTable updates={updates?.updates ?? []} />
|
||||
<Page
|
||||
title="Updates"
|
||||
actions={
|
||||
<OperationSelector selected={operation} onSelect={setOperation} />
|
||||
}
|
||||
>
|
||||
<UpdatesTable updates={updates?.updates ?? []} showTarget />
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -35,24 +53,141 @@ const ResourceUpdates = ({
|
||||
}) => {
|
||||
const name = useRead(`List${type}s`, {}).data?.find((r) => r.id === id)?.name;
|
||||
useSetTitle(name && `${name} | Updates`);
|
||||
const [operation, setOperation] = useState<Types.Operation | undefined>();
|
||||
const updates = useRead("ListUpdates", {
|
||||
query: {
|
||||
"target.type": type,
|
||||
"target.id": id,
|
||||
operation,
|
||||
},
|
||||
}).data;
|
||||
const Components = ResourceComponents[type];
|
||||
return (
|
||||
<Page
|
||||
title={<Components.Name id={id} />}
|
||||
titleRight={
|
||||
<div className="flex gap-2">
|
||||
<ResourceTags target={{ id, type }} click_to_delete />
|
||||
<AddTags target={{ id, type }} />
|
||||
</div>
|
||||
titleRight={<h2 className="text-muted-foreground">Updates</h2>}
|
||||
icon={<Components.BigIcon id={id} />}
|
||||
actions={
|
||||
<OperationSelector
|
||||
selected={operation}
|
||||
onSelect={setOperation}
|
||||
options={OPERATIONS_BY_RESOURCE[type]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<UpdatesTable updates={updates?.updates ?? []} />
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
const OPERATIONS_BY_RESOURCE: { [key: string]: Types.Operation[] } = {
|
||||
Server: [
|
||||
Types.Operation.CreateServer,
|
||||
Types.Operation.UpdateServer,
|
||||
Types.Operation.RenameServer,
|
||||
Types.Operation.PruneImagesServer,
|
||||
Types.Operation.PruneContainersServer,
|
||||
Types.Operation.PruneNetworksServer,
|
||||
Types.Operation.CreateNetwork,
|
||||
Types.Operation.DeleteNetwork,
|
||||
Types.Operation.StopAllContainers,
|
||||
],
|
||||
Deployment: [
|
||||
Types.Operation.CreateDeployment,
|
||||
Types.Operation.UpdateDeployment,
|
||||
Types.Operation.RenameDeployment,
|
||||
Types.Operation.DeployContainer,
|
||||
Types.Operation.StopContainer,
|
||||
Types.Operation.StartContainer,
|
||||
Types.Operation.RemoveContainer,
|
||||
],
|
||||
Build: [
|
||||
Types.Operation.CreateBuild,
|
||||
Types.Operation.UpdateBuild,
|
||||
Types.Operation.RunBuild,
|
||||
Types.Operation.CancelBuild,
|
||||
],
|
||||
Repo: [
|
||||
Types.Operation.CreateRepo,
|
||||
Types.Operation.UpdateRepo,
|
||||
Types.Operation.CloneRepo,
|
||||
Types.Operation.PullRepo,
|
||||
],
|
||||
Procedure: [
|
||||
Types.Operation.CreateProcedure,
|
||||
Types.Operation.UpdateProcedure,
|
||||
Types.Operation.RunProcedure,
|
||||
],
|
||||
Builder: [Types.Operation.CreateBuilder, Types.Operation.UpdateBuilder],
|
||||
Alerter: [Types.Operation.CreateAlerter, Types.Operation.UpdateAlerter],
|
||||
ServerTemplate: [
|
||||
Types.Operation.CreateServerTemplate,
|
||||
Types.Operation.UpdateServerTemplate,
|
||||
Types.Operation.LaunchServer,
|
||||
],
|
||||
};
|
||||
|
||||
const OperationSelector = ({
|
||||
selected,
|
||||
onSelect,
|
||||
options = Object.values(Types.Operation).filter(
|
||||
(o) => o !== Types.Operation.None
|
||||
),
|
||||
}: {
|
||||
selected: Types.Operation | undefined;
|
||||
onSelect: (operation: Types.Operation | undefined) => void;
|
||||
options?: Types.Operation[];
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [input, setInput] = useState("");
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="h-full w-[200px] cursor-pointer flex items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1">
|
||||
{selected ?? "Select Operation"}
|
||||
<CaretSortIcon className="h-4 w-4 opacity-50" />
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="w-[200px] max-h-[200px] p-0">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search Operations"
|
||||
value={input}
|
||||
onValueChange={setInput}
|
||||
className="h-9"
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty className="flex justify-evenly items-center">
|
||||
No Operations Found
|
||||
<SearchX className="w-3 h-3" />
|
||||
</CommandEmpty>
|
||||
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
className="cursor-pointer"
|
||||
onSelect={() => {
|
||||
onSelect(undefined);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<div>All</div>
|
||||
</CommandItem>
|
||||
|
||||
{options.map((operation) => (
|
||||
<CommandItem
|
||||
className="cursor-pointer"
|
||||
onSelect={() => {
|
||||
onSelect(operation);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{operation}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user