mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-01-26 01:21:04 +00:00
Update redirect logic, fix exit node dropdown, fix route search
This commit is contained in:
@@ -23,6 +23,7 @@ import Separator from "@components/Separator";
|
|||||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||||
import LoginExpiredBadge from "@components/ui/LoginExpiredBadge";
|
import LoginExpiredBadge from "@components/ui/LoginExpiredBadge";
|
||||||
import TextWithTooltip from "@components/ui/TextWithTooltip";
|
import TextWithTooltip from "@components/ui/TextWithTooltip";
|
||||||
|
import useRedirect from "@hooks/useRedirect";
|
||||||
import { IconCloudLock, IconInfoCircle } from "@tabler/icons-react";
|
import { IconCloudLock, IconInfoCircle } from "@tabler/icons-react";
|
||||||
import useFetchApi from "@utils/api";
|
import useFetchApi from "@utils/api";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
@@ -66,8 +67,11 @@ import PeerRoutesTable from "@/modules/peer/PeerRoutesTable";
|
|||||||
export default function PeerPage() {
|
export default function PeerPage() {
|
||||||
const queryParameter = useSearchParams();
|
const queryParameter = useSearchParams();
|
||||||
const peerId = queryParameter.get("id");
|
const peerId = queryParameter.get("id");
|
||||||
const { data: peer } = useFetchApi<Peer>("/peers/" + peerId);
|
const { data: peer, isLoading } = useFetchApi<Peer>("/peers/" + peerId, true);
|
||||||
return peer ? (
|
|
||||||
|
useRedirect("/peers", false, !peerId);
|
||||||
|
|
||||||
|
return peer && !isLoading ? (
|
||||||
<PeerProvider peer={peer}>
|
<PeerProvider peer={peer}>
|
||||||
<PeerOverview />
|
<PeerOverview />
|
||||||
</PeerProvider>
|
</PeerProvider>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import Paragraph from "@components/Paragraph";
|
|||||||
import { PeerGroupSelector } from "@components/PeerGroupSelector";
|
import { PeerGroupSelector } from "@components/PeerGroupSelector";
|
||||||
import Separator from "@components/Separator";
|
import Separator from "@components/Separator";
|
||||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||||
|
import useRedirect from "@hooks/useRedirect";
|
||||||
import { IconCirclePlus, IconSettings2 } from "@tabler/icons-react";
|
import { IconCirclePlus, IconSettings2 } from "@tabler/icons-react";
|
||||||
import useFetchApi, { useApiCall } from "@utils/api";
|
import useFetchApi, { useApiCall } from "@utils/api";
|
||||||
import { generateColorFromString } from "@utils/helpers";
|
import { generateColorFromString } from "@utils/helpers";
|
||||||
@@ -42,6 +43,8 @@ export default function UserPage() {
|
|||||||
return users?.find((u) => u.id === userId);
|
return users?.find((u) => u.id === userId);
|
||||||
}, [users, userId]);
|
}, [users, userId]);
|
||||||
|
|
||||||
|
useRedirect("/team/users", false, !userId);
|
||||||
|
|
||||||
return !isLoading && user ? (
|
return !isLoading && user ? (
|
||||||
<UserOverview user={user} />
|
<UserOverview user={user} />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -35,6 +35,6 @@ export default function NotFound() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Redirect = ({ url, queryParams }: Props) => {
|
const Redirect = ({ url, queryParams }: Props) => {
|
||||||
useRedirect(url == "/" ? "/peers" : url + (queryParams && `?${queryParams}`));
|
useRedirect("/peers" + (queryParams && `?${queryParams}`));
|
||||||
return <FullScreenLoading />;
|
return <FullScreenLoading />;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export interface InputProps
|
|||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
error?: string;
|
error?: string;
|
||||||
errorTooltip?: boolean;
|
errorTooltip?: boolean;
|
||||||
errorTooltipPosition?: "top" | "top-right";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputVariants = cva("", {
|
const inputVariants = cva("", {
|
||||||
@@ -50,7 +49,6 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
maxWidthClass = "",
|
maxWidthClass = "",
|
||||||
error,
|
error,
|
||||||
errorTooltip = false,
|
errorTooltip = false,
|
||||||
errorTooltipPosition = "top",
|
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
@@ -107,12 +105,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
</div>
|
</div>
|
||||||
{error && errorTooltip && (
|
{error && errorTooltip && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={
|
||||||
errorTooltipPosition == "top" &&
|
"absolute right-0 top-2 h-[0px] w-full flex items-center pr-3 justify-center"
|
||||||
"absolute right-0 top-2 h-[0px] w-full flex items-center pr-3 justify-center",
|
}
|
||||||
errorTooltipPosition == "top-right" &&
|
|
||||||
"absolute -right-6 top-2 h-[0px] w-full flex items-center pr-3 justify-end",
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<FullTooltip
|
<FullTooltip
|
||||||
content={
|
content={
|
||||||
@@ -125,7 +120,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
interactive={false}
|
interactive={false}
|
||||||
align={errorTooltipPosition == "top" ? "center" : "end"}
|
align={"center"}
|
||||||
side={"top"}
|
side={"top"}
|
||||||
keepOpen={true}
|
keepOpen={true}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import loadConfig from "@utils/config";
|
import loadConfig from "@utils/config";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
export const useRedirect = (
|
export const useRedirect = (
|
||||||
url: string,
|
url: string,
|
||||||
replace: boolean = false,
|
replace: boolean = false,
|
||||||
@@ -10,24 +11,43 @@ export const useRedirect = (
|
|||||||
) => {
|
) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const currentPath = usePathname();
|
const currentPath = usePathname();
|
||||||
const callBackUrls = [config.redirectURI, config.silentRedirectURI];
|
const callBackUrls = useRef([config.redirectURI, config.silentRedirectURI]);
|
||||||
|
const isRedirecting = useRef(false);
|
||||||
|
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enable) return;
|
// If redirect is disabled or the url is already in the callback urls or the url is the current path then do not redirect
|
||||||
if (callBackUrls.includes(url)) return; // Don't redirect to the callback urls to avoid infinite loop
|
if (!enable || callBackUrls.current.includes(url) || url === currentPath)
|
||||||
if (url === currentPath) return; // Don't redirect to the current page
|
return;
|
||||||
|
|
||||||
const redirect = replace ? router.replace : router.push; // Replace the current history or add a new one
|
const performRedirect = () => {
|
||||||
|
if (!isRedirecting.current) {
|
||||||
|
isRedirecting.current = true;
|
||||||
|
router.refresh();
|
||||||
|
if (replace) {
|
||||||
|
router.replace(url);
|
||||||
|
} else {
|
||||||
|
router.push(url);
|
||||||
|
}
|
||||||
|
isRedirecting.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
router.refresh();
|
performRedirect();
|
||||||
redirect(url);
|
|
||||||
|
|
||||||
// Timer in case the user has his browser tab open but not focused
|
// Try to redirect after 1.25 seconds if for whatever reason the redirect did not happen (network change, browser tab open but not focused etc.)
|
||||||
const interval = setInterval(() => {
|
intervalRef.current = setInterval(() => {
|
||||||
router.refresh();
|
if (!isRedirecting.current) {
|
||||||
redirect(url);
|
performRedirect();
|
||||||
}, 1000);
|
}
|
||||||
|
}, 1250);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => {
|
||||||
}, [replace, router, url, enable]);
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [replace, router, url, enable, currentPath]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default useRedirect;
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import { Label } from "@components/Label";
|
|||||||
import { IconInfoCircle } from "@tabler/icons-react";
|
import { IconInfoCircle } from "@tabler/icons-react";
|
||||||
import { cn } from "@utils/helpers";
|
import { cn } from "@utils/helpers";
|
||||||
import { isLocalDev, isProduction } from "@utils/netbird";
|
import { isLocalDev, isProduction } from "@utils/netbird";
|
||||||
|
import { isEmpty } from "lodash";
|
||||||
|
import { GlobeIcon } from "lucide-react";
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
|
import RoundedFlag from "@/assets/countries/RoundedFlag";
|
||||||
|
import { useCountries } from "@/contexts/CountryProvider";
|
||||||
import { ActivityEvent } from "@/interfaces/ActivityEvent";
|
import { ActivityEvent } from "@/interfaces/ActivityEvent";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -54,7 +58,8 @@ export default function ActivityDescription({ event }: Props) {
|
|||||||
if (event.activity_code == "setupkey.peer.add")
|
if (event.activity_code == "setupkey.peer.add")
|
||||||
return (
|
return (
|
||||||
<div className={"inline"}>
|
<div className={"inline"}>
|
||||||
Peer <Value>{m.name}</Value> with ip <Value>{m.ip}</Value> was added
|
Peer <Value>{m.name}</Value> <PeerConnectionInfo meta={m} /> was added
|
||||||
|
with the NetBird IP <Value>{m.ip}</Value>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -144,21 +149,24 @@ export default function ActivityDescription({ event }: Props) {
|
|||||||
if (event.activity_code == "user.peer.delete")
|
if (event.activity_code == "user.peer.delete")
|
||||||
return (
|
return (
|
||||||
<div className={"inline"}>
|
<div className={"inline"}>
|
||||||
Peer <Value>{m.name}</Value> with ip <Value>{m.ip}</Value> was deleted
|
Peer <Value>{m.name}</Value> <PeerConnectionInfo meta={m} /> with
|
||||||
|
NetBird IP <Value>{m.ip}</Value> was deleted
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (event.activity_code == "user.peer.add")
|
if (event.activity_code == "user.peer.add")
|
||||||
return (
|
return (
|
||||||
<div className={"inline"}>
|
<div className={"inline"}>
|
||||||
Peer <Value>{m.name}</Value> with ip <Value>{m.ip}</Value> was added
|
Peer <Value>{m.name}</Value> <PeerConnectionInfo meta={m} /> was added
|
||||||
|
with the NetBird IP <Value>{m.ip}</Value>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (event.activity_code == "user.peer.update")
|
if (event.activity_code == "user.peer.update")
|
||||||
return (
|
return (
|
||||||
<div className={"inline"}>
|
<div className={"inline"}>
|
||||||
Peer <Value>{m.name}</Value> with ip <Value>{m.ip}</Value> was updated
|
Peer <Value>{m.name}</Value> <PeerConnectionInfo meta={m} /> with
|
||||||
|
NetBird IP <Value>{m.ip}</Value> was updated
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -252,15 +260,15 @@ export default function ActivityDescription({ event }: Props) {
|
|||||||
if (event.activity_code == "peer.group.delete")
|
if (event.activity_code == "peer.group.delete")
|
||||||
return (
|
return (
|
||||||
<div className={"inline"}>
|
<div className={"inline"}>
|
||||||
Group <Value>{m.group}</Value> was removed from the peer with the ip{" "}
|
Group <Value>{m.group}</Value> was removed from the peer with the
|
||||||
<Value>{m.peer_ip}</Value>
|
NetBird IP <Value>{m.peer_ip}</Value>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (event.activity_code == "peer.group.add")
|
if (event.activity_code == "peer.group.add")
|
||||||
return (
|
return (
|
||||||
<div className={"inline"}>
|
<div className={"inline"}>
|
||||||
Group <Value>{m.group}</Value> was added to the peer with the ip{" "}
|
Group <Value>{m.group}</Value> was added to the peer with the NetBird IP{" "}
|
||||||
<Value>{m.peer_ip}</Value>
|
<Value>{m.peer_ip}</Value>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -303,7 +311,7 @@ export default function ActivityDescription({ event }: Props) {
|
|||||||
if (event.activity_code == "peer.rename")
|
if (event.activity_code == "peer.rename")
|
||||||
return (
|
return (
|
||||||
<div className={"inline"}>
|
<div className={"inline"}>
|
||||||
Peer with the ip <Value>{m.ip}</Value> was renamed to{" "}
|
Peer with the NetBird IP <Value>{m.ip}</Value> was renamed to{" "}
|
||||||
<Value>{m.name}</Value>
|
<Value>{m.name}</Value>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -311,7 +319,7 @@ export default function ActivityDescription({ event }: Props) {
|
|||||||
if (event.activity_code == "peer.approve")
|
if (event.activity_code == "peer.approve")
|
||||||
return (
|
return (
|
||||||
<div className={"inline"}>
|
<div className={"inline"}>
|
||||||
Peer with the ip <Value>{m.ip}</Value> was approved
|
Peer with the NetBird IP <Value>{m.ip}</Value> was approved
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -559,7 +567,7 @@ function Value({
|
|||||||
return children ? (
|
return children ? (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-nb-gray-200 inline font-medium bg-nb-gray-900 py-[3px] text-[11px] px-[5px] border border-nb-gray-800 rounded-[4px]",
|
"text-nb-gray-200 inline-flex gap-1 items-center max-h-[22px] font-medium bg-nb-gray-900 py-[3px] text-[11px] px-[5px] border border-nb-gray-800 rounded-[4px]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -567,3 +575,40 @@ function Value({
|
|||||||
</span>
|
</span>
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PeerConnectionInfo({ meta }: { meta: any }) {
|
||||||
|
const hasMeta =
|
||||||
|
!isEmpty(meta?.location_country_code) ||
|
||||||
|
!isEmpty(meta?.location_connection_ip);
|
||||||
|
const { countries } = useCountries();
|
||||||
|
|
||||||
|
const countryText = useMemo(() => {
|
||||||
|
if (!countries) return "Unknown";
|
||||||
|
const country = countries.find(
|
||||||
|
(c) => c.country_code === meta?.location_country_code,
|
||||||
|
);
|
||||||
|
if (!country) return "Unknown";
|
||||||
|
if (!meta?.location_city_name) return country.country_name;
|
||||||
|
return `${country.country_name}, ${meta?.location_city_name}`;
|
||||||
|
}, [countries, meta]);
|
||||||
|
|
||||||
|
return hasMeta ? (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
from{" "}
|
||||||
|
{meta?.location_connection_ip && (
|
||||||
|
<Value>{meta?.location_connection_ip}</Value>
|
||||||
|
)}{" "}
|
||||||
|
{meta?.location_country_code && (
|
||||||
|
<Value>
|
||||||
|
{isEmpty(meta?.location_country_code) ? (
|
||||||
|
<GlobeIcon size={9} className={"text-nb-gray-300"} />
|
||||||
|
) : (
|
||||||
|
<RoundedFlag country={meta?.location_country_code} size={9} />
|
||||||
|
)}
|
||||||
|
{countryText}
|
||||||
|
</Value>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { DropdownMenuItem } from "@components/DropdownMenu";
|
import { DropdownMenuItem } from "@components/DropdownMenu";
|
||||||
import { Modal } from "@components/modal/Modal";
|
import { Modal } from "@components/modal/Modal";
|
||||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
||||||
import { IconDirectionSign } from "@tabler/icons-react";
|
import { IconCirclePlus, IconDirectionSign } from "@tabler/icons-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import RoutesProvider from "@/contexts/RoutesProvider";
|
||||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||||
import { Peer } from "@/interfaces/Peer";
|
import { Peer } from "@/interfaces/Peer";
|
||||||
|
import { useHasExitNodes } from "@/modules/exit-node/useHasExitNodes";
|
||||||
import { RouteModalContent } from "@/modules/routes/RouteModal";
|
import { RouteModalContent } from "@/modules/routes/RouteModal";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -15,24 +17,41 @@ type Props = {
|
|||||||
export const ExitNodeDropdownButton = ({ peer }: Props) => {
|
export const ExitNodeDropdownButton = ({ peer }: Props) => {
|
||||||
const [modal, setModal] = useState(false);
|
const [modal, setModal] = useState(false);
|
||||||
const isLinux = getOperatingSystem(peer.os) === OperatingSystem.LINUX;
|
const isLinux = getOperatingSystem(peer.os) === OperatingSystem.LINUX;
|
||||||
|
const hasExitNodes = useHasExitNodes(peer);
|
||||||
|
|
||||||
return isLinux ? (
|
return isLinux ? (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuItem onClick={() => setModal(true)}>
|
<DropdownMenuItem onClick={() => setModal(true)}>
|
||||||
<div className={"flex gap-3 items-center w-full"}>
|
<div className={"flex gap-3 items-center w-full"}>
|
||||||
<IconDirectionSign size={14} className={"shrink-0"} />
|
{hasExitNodes ? (
|
||||||
<div className={"flex justify-between items-center w-full"}>
|
<>
|
||||||
Add Exit Node
|
<IconCirclePlus size={14} className={"shrink-0"} />
|
||||||
</div>
|
<div className={"flex justify-between items-center w-full"}>
|
||||||
|
Add Exit Node
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<IconDirectionSign
|
||||||
|
size={14}
|
||||||
|
className={"shrink-0 text-yellow-400"}
|
||||||
|
/>
|
||||||
|
<div className={"flex justify-between items-center w-full"}>
|
||||||
|
Setup Exit Node
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<Modal open={modal} onOpenChange={setModal}>
|
<Modal open={modal} onOpenChange={setModal}>
|
||||||
{modal && (
|
{modal && (
|
||||||
<RouteModalContent
|
<RoutesProvider>
|
||||||
onSuccess={() => setModal(false)}
|
<RouteModalContent
|
||||||
peer={peer}
|
onSuccess={() => setModal(false)}
|
||||||
exitNode={true}
|
peer={peer}
|
||||||
/>
|
exitNode={true}
|
||||||
|
/>
|
||||||
|
</RoutesProvider>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ export const RouteTableColumns: ColumnDef<Route>[] = [
|
|||||||
sortingFn: "text",
|
sortingFn: "text",
|
||||||
cell: ({ row }) => <RoutePeerCell route={row.original} />,
|
cell: ({ row }) => <RoutePeerCell route={row.original} />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "description",
|
||||||
|
sortingFn: "text",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "metric",
|
accessorKey: "metric",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
@@ -122,6 +126,7 @@ export default function RouteTable({ row }: Props) {
|
|||||||
sorting={sorting}
|
sorting={sorting}
|
||||||
columnVisibility={{
|
columnVisibility={{
|
||||||
group_names: false,
|
group_names: false,
|
||||||
|
description: false,
|
||||||
}}
|
}}
|
||||||
onRowClick={(row, cell) => {
|
onRowClick={(row, cell) => {
|
||||||
setCurrentRow(row.original);
|
setCurrentRow(row.original);
|
||||||
|
|||||||
16
src/modules/setup-keys/SetupKeyKeyCell.tsx
Normal file
16
src/modules/setup-keys/SetupKeyKeyCell.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import Badge from "@components/Badge";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SetupKeyKeyCell({ text }: Props) {
|
||||||
|
return (
|
||||||
|
<div className={"flex"}>
|
||||||
|
<Badge variant={"gray"} className={"text-xs font-mono"}>
|
||||||
|
{text.substring(0, 5) + "****"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
src/modules/setup-keys/SetupKeyTypeCell.tsx
Normal file
24
src/modules/setup-keys/SetupKeyTypeCell.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import Badge from "@components/Badge";
|
||||||
|
import { IconRepeat } from "@tabler/icons-react";
|
||||||
|
import { Repeat1 } from "lucide-react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
reusable: boolean;
|
||||||
|
};
|
||||||
|
export default function SetupKeyTypeCell({ reusable }: Props) {
|
||||||
|
return (
|
||||||
|
<div className={"flex"}>
|
||||||
|
<Badge className={"text-xs"} variant={"gray"}>
|
||||||
|
{reusable ? (
|
||||||
|
<>
|
||||||
|
<IconRepeat size={14} className={"text-green-400"} /> Reusable
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Repeat1 size={14} /> One-off
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user