Compare commits

..
72 changed files with 3755 additions and 364 deletions

No files matched your search

+1 -2
View File
@@ -12,6 +12,5 @@
"tokenSource": "$NETBIRD_TOKEN_SOURCE",
"dragQueryParams": "$NETBIRD_DRAG_QUERY_PARAMS",
"hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID",
"googleAnalyticsID": "$NETBIRD_GOOGLE_ANALYTICS_ID",
"authServiceUrl": "$NETBIRD_AUTH_SERVICE_URL"
"googleAnalyticsID": "$NETBIRD_GOOGLE_ANALYTICS_ID"
}
+372 -195
View File
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -55,8 +55,8 @@
"framer-motion": "^10.16.4",
"ip-cidr": "^3.1.0",
"lodash": "^4.17.21",
"lucide-react": "^0.383.0",
"next": "13.5.5",
"lucide-react": "^0.460.0",
"next": "13.5.7",
"next-themes": "^0.2.1",
"punycode": "^2.3.1",
"react": "^18",
@@ -76,7 +76,7 @@
"typescript": "^5"
},
"devDependencies": {
"cypress": "^13.3.3",
"cypress": "^13.13.0",
"postcss": "^8",
"prettier": "3.0.3",
"tailwindcss": "^3"
+4 -1
View File
@@ -14,6 +14,7 @@ import PeersProvider from "@/contexts/PeersProvider";
import RoutesProvider from "@/contexts/RoutesProvider";
import { Route } from "@/interfaces/Route";
import PageContainer from "@/layouts/PageContainer";
import { NetworkRoutesDeprecationInfo } from "@/modules/networks/misc/NetworkRoutesDeprecationInfo";
import useGroupedRoutes from "@/modules/route-group/useGroupedRoutes";
const NetworkRoutesTable = lazy(
@@ -39,7 +40,9 @@ export default function NetworkRoutes() {
icon={<NetworkRoutesIcon size={13} />}
/>
</Breadcrumbs>
<h1 ref={headingRef}>Network Routes</h1>
<h1 ref={headingRef}>
Network Routes <NetworkRoutesDeprecationInfo size={18} />
</h1>
<Paragraph>
Network routes allow you to access other networks like LANs and
VPCs without installing NetBird on every resource.
+8
View File
@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Network - Networks - ${globalMetaTitle}`,
};
export default BlankLayout;
+229
View File
@@ -0,0 +1,229 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import Card from "@components/Card";
import FullTooltip from "@components/FullTooltip";
import InlineLink from "@components/InlineLink";
import Separator from "@components/Separator";
import FullScreenLoading from "@components/ui/FullScreenLoading";
import useRedirect from "@hooks/useRedirect";
import useFetchApi from "@utils/api";
import { cn } from "@utils/helpers";
import {
ArrowUpRightIcon,
HelpCircle,
PencilLineIcon,
ServerIcon,
ShieldCheckIcon,
ShieldXIcon,
} from "lucide-react";
import { useSearchParams } from "next/navigation";
import React, { useMemo, useState } from "react";
import { useSWRConfig } from "swr";
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
import { useLoggedInUser } from "@/contexts/UsersProvider";
import { Network } from "@/interfaces/Network";
import PageContainer from "@/layouts/PageContainer";
import { NetworkInformationSquare } from "@/modules/networks/misc/NetworkInformationSquare";
import NetworkModal from "@/modules/networks/NetworkModal";
import { NetworkProvider } from "@/modules/networks/NetworkProvider";
import { ResourcesSection } from "@/modules/networks/resources/ResourcesSection";
import { NetworkRoutingPeersSection } from "@/modules/networks/routing-peers/NetworkRoutingPeersSection";
export default function NetworkDetailPage() {
const queryParameter = useSearchParams();
const networkId = queryParameter.get("id");
const { data: network, isLoading } = useFetchApi<Network>(
`/networks/${networkId}`,
true,
);
useRedirect("/networks", false, !networkId);
return network && !isLoading ? (
<NetworkOverview network={network} />
) : (
<FullScreenLoading />
);
}
function NetworkOverview({ network }: Readonly<{ network: Network }>) {
const { isUser } = useLoggedInUser();
const [networkModal, setNetworkModal] = useState(false);
const { mutate } = useSWRConfig();
const isActive = !!(
network?.routing_peers_count && network.routing_peers_count > 0
);
return (
<PageContainer>
<NetworkProvider network={network}>
<div className={"p-default py-6 mb-4"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/networks"}
label={"Networks"}
disabled={isUser}
icon={<NetworkRoutesIcon size={13} />}
/>
<Breadcrumbs.Item
href={"/network"}
label={network.name}
active={true}
/>
</Breadcrumbs>
<div className={"flex justify-between max-w-6xl"}>
<div
className={cn(
"flex items-center",
!network.description && "gap-2",
)}
>
<NetworkInformationSquare
name={network.name}
active={isActive}
size={"lg"}
description={network.description}
/>
<button
className={
"flex items-center gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 py-2 px-3 rounded-md cursor-pointer"
}
onClick={() => setNetworkModal(true)}
>
<PencilLineIcon size={18} />
</button>
<NetworkModal
open={networkModal}
setOpen={setNetworkModal}
onUpdated={() => {
mutate(`/networks/${network.id}`);
}}
network={network}
/>
</div>
</div>
<div className={"flex gap-10 w-full mt-8 max-w-6xl items-start"}>
<NetworkInformationCard network={network} />
</div>
</div>
<Separator />
<ResourcesSection network={network} />
<div className={"h-3"} />
<Separator />
<NetworkRoutingPeersSection network={network} />
</NetworkProvider>
</PageContainer>
);
}
function NetworkInformationCard({ network }: Readonly<{ network: Network }>) {
const isHighlyAvailable = !!(
network?.routing_peers_count && network?.routing_peers_count >= 2
);
const disabledText = useMemo(
() => (
<>
High availability is currently{" "}
<span className={"text-yellow-400 font-medium"}>inactive</span> for this
network.
</>
),
[],
);
const enabledText = useMemo(
() => (
<>
High availability is{" "}
<span className={"text-green-500 font-medium"}>active</span> for this
network.
</>
),
[],
);
const policyCount = network.policies?.length ?? 0;
return (
<Card>
<Card.List>
<Card.ListItem
tooltip={false}
label={
<>
<ServerIcon size={16} />
High Availability
</>
}
value={
<FullTooltip
interactive={false}
content={
<div className={"max-w-xs text-xs"}>
{isHighlyAvailable ? enabledText : disabledText}
{isHighlyAvailable ? (
<div className={"inline-flex mt-2"}>
You can add more routing peers to increase the
availability of this network.
</div>
) : (
<div className={"inline-flex mt-2"}>
Go ahead and add more routing peers or groups with routing
peers to enable high availability for this network.
</div>
)}
</div>
}
>
<div
className={cn(
"flex gap-2.5 items-center text-nb-gray-300 text-sm cursor-help",
)}
>
<span
className={cn(
"h-2 w-2 rounded-full",
!isHighlyAvailable ? "bg-yellow-400" : "bg-green-500",
)}
></span>
{isHighlyAvailable ? "Active" : "Inactive"}
<HelpCircle size={12} />
</div>
</FullTooltip>
}
/>
<Card.ListItem
tooltip={false}
label={
policyCount > 0 ? (
<>
<ShieldCheckIcon size={16} className={"text-green-500"} />
{policyCount}{" "}
{policyCount === 1 ? "Active Policy" : "Active Policies"}
</>
) : (
<>
<ShieldXIcon size={16} className={"text-red-500"} />
No Active Policies
</>
)
}
value={
policyCount > 0 ? (
<InlineLink href={"/access-control"}>
Go to Policies
<ArrowUpRightIcon size={14} />
</InlineLink>
) : null
}
/>
</Card.List>
</Card>
);
}
+8
View File
@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Networks - ${globalMetaTitle}`,
};
export default BlankLayout;
+58
View File
@@ -0,0 +1,58 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph";
import SkeletonTable from "@components/skeletons/SkeletonTable";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import { usePortalElement } from "@hooks/usePortalElement";
import useFetchApi from "@utils/api";
import { ExternalLinkIcon } from "lucide-react";
import React, { Suspense } from "react";
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
import { Network } from "@/interfaces/Network";
import PageContainer from "@/layouts/PageContainer";
import NetworksTable from "@/modules/networks/table/NetworksTable";
export default function Networks() {
const { data: networks, isLoading } = useFetchApi<Network[]>("/networks");
const { ref: headingRef, portalTarget } =
usePortalElement<HTMLHeadingElement>();
return (
<PageContainer>
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/networks"}
label={"Networks"}
icon={<NetworkRoutesIcon size={13} />}
/>
</Breadcrumbs>
<h1 ref={headingRef}>Networks</h1>
<Paragraph>
Networks allow you to access other resources like LANs and VPCs
without installing NetBird on every device.
</Paragraph>
<Paragraph>
Learn more about
<InlineLink href={"https://docs.netbird.io/how-to/networks"} target={"_blank"}>
Networks
<ExternalLinkIcon size={12} />
</InlineLink>
in our documentation.
</Paragraph>
</div>
<RestrictedAccess>
<Suspense fallback={<SkeletonTable />}>
<NetworksTable
data={networks}
isLoading={isLoading}
headingTarget={portalTarget}
/>
</Suspense>
</RestrictedAccess>
</PageContainer>
);
}
+8 -25
View File
@@ -297,29 +297,12 @@ function PeerOverview() {
/>
</FullTooltip>
<div>
<Label>Assigned Groups</Label>
<HelpText>
Use groups to control what this peer can access.
</HelpText>
<FullTooltip
content={
<div
className={
"flex gap-2 items-center !text-nb-gray-300 text-xs"
}
>
<LockIcon size={14} />
<span>
{`You don't have the required permissions to update this
setting.`}
</span>
</div>
}
interactive={false}
className={"w-full block"}
disabled={!isUser}
>
{!isUser && (
<div>
<Label>Assigned Groups</Label>
<HelpText>
Use groups to control what this peer can access.
</HelpText>
<PeerGroupSelector
disabled={isUser}
onChange={setSelectedGroups}
@@ -327,8 +310,8 @@ function PeerOverview() {
hideAllGroup={true}
peer={peer}
/>
</FullTooltip>
</div>
</div>
)}
</div>
</div>
</div>
+8 -5
View File
@@ -6,17 +6,18 @@ import {
AlertOctagonIcon,
FolderGit2Icon,
LockIcon,
NetworkIcon,
ShieldIcon,
} from "lucide-react";
import { useSearchParams } from "next/navigation";
import React, { useEffect, useState } from "react";
import { useLoggedInUser } from "@/contexts/UsersProvider";
import { Role } from "@/interfaces/User";
import PageContainer from "@/layouts/PageContainer";
import { useAccount } from "@/modules/account/useAccount";
import AuthenticationTab from "@/modules/settings/AuthenticationTab";
import DangerZoneTab from "@/modules/settings/DangerZoneTab";
import GroupsTab from "@/modules/settings/GroupsTab";
import NetworkSettingsTab from "@/modules/settings/NetworkSettingsTab";
import PermissionsTab from "@/modules/settings/PermissionsTab";
export default function NetBirdSettings() {
@@ -48,19 +49,21 @@ export default function NetBirdSettings() {
<LockIcon size={14} />
Permissions
</VerticalTabs.Trigger>
<VerticalTabs.Trigger value="networks">
<NetworkIcon size={14} />
Networks
</VerticalTabs.Trigger>
<VerticalTabs.Trigger value="danger-zone" disabled={!isOwner}>
<AlertOctagonIcon size={14} />
Danger zone
</VerticalTabs.Trigger>
</VerticalTabs.List>
<RestrictedAccess
page={"Settings"}
allow={[Role.Admin, Role.Owner, Role.BillingAdmin]}
>
<RestrictedAccess page={"Settings"}>
<div className={"border-l border-nb-gray-930 w-full"}>
{account && <AuthenticationTab account={account} />}
{account && <PermissionsTab account={account} />}
{account && <GroupsTab account={account} />}
{account && <NetworkSettingsTab account={account} />}
{account && <DangerZoneTab account={account} />}
</div>
</RestrictedAccess>
+9 -2
View File
@@ -15,7 +15,9 @@ export const OIDCError = () => {
const params = useSearchParams();
const errorParam = params.get("error");
const accessDenied = errorParam === "access_denied";
const invalidRequest = errorParam === "invalid_request";
const [title, setTitle] = useState(params.get("error_description"));
const errorDescription = params.get("error_description");
const { logout, login } = useOidc();
useEffect(() => {
@@ -72,9 +74,14 @@ export const OIDCError = () => {
</>
) : (
<>
<Paragraph className={"text-center mt-2"}>
<Paragraph className={"text-center mt-2 block"}>
There was an error logging you in. <br />
Error: {oidcUserLoadingState}
Error:{" "}
<span className={"inline capitalize"}>
{invalidRequest && errorDescription
? errorDescription
: oidcUserLoadingState}
</span>
</Paragraph>
<Button
variant={"primary"}
+5
View File
@@ -35,6 +35,11 @@ export const buttonVariants = cva(
"dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20 ",
"dark:bg-nb-gray-900/30 dark:text-gray-400 dark:border-gray-700/40 dark:hover:text-white dark:hover:bg-zinc-800/50",
],
secondaryLighter: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900",
"dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20 ",
"dark:bg-nb-gray-900/70 dark:text-gray-400 dark:border-gray-700/70 dark:hover:text-white dark:hover:bg-nb-gray-800/60",
],
input: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-neutral-200 text-gray-900",
"dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20 ",
+14 -3
View File
@@ -1,12 +1,15 @@
import FullTooltip from "@components/FullTooltip";
import Paragraph from "@components/Paragraph";
import { cn } from "@utils/helpers";
import { cva } from "class-variance-authority";
import { cva, VariantProps } from "class-variance-authority";
import { AlertCircle } from "lucide-react";
import * as React from "react";
type InputVariants = VariantProps<typeof inputVariants>;
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
extends React.InputHTMLAttributes<HTMLInputElement>,
InputVariants {
customPrefix?: React.ReactNode;
customSuffix?: React.ReactNode;
maxWidthClass?: string;
@@ -14,6 +17,7 @@ export interface InputProps
error?: string;
errorTooltip?: boolean;
errorTooltipPosition?: "top" | "top-right";
prefixClassName?: string;
}
const inputVariants = cva("", {
@@ -23,6 +27,10 @@ const inputVariants = cva("", {
"dark:bg-nb-gray-900 dark:placeholder:text-neutral-400/70 placeholder:text-neutral-500 border-neutral-200 dark:border-nb-gray-700",
"ring-offset-neutral-200/20 dark:ring-offset-neutral-950/50 dark:focus-visible:ring-neutral-500/20 focus-visible:ring-neutral-300/10",
],
darker: [
"dark:bg-nb-gray-920 dark:placeholder:text-neutral-400/70 placeholder:text-neutral-500 border-neutral-300 dark:border-nb-gray-800",
"ring-offset-neutral-200/20 dark:ring-offset-neutral-950/50 dark:focus-visible:ring-neutral-500/20 focus-visible:ring-neutral-300/10",
],
error: [
"dark:bg-nb-gray-900 dark:placeholder:text-neutral-400/70 placeholder:text-neutral-500 border-neutral-200 dark:border-red-500 text-red-500",
"ring-offset-red-500/10 dark:ring-offset-red-500/10 dark:focus-visible:ring-red-500/10 focus-visible:ring-red-500/10",
@@ -51,6 +59,8 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
error,
errorTooltip = false,
errorTooltipPosition = "top",
variant = "default",
prefixClassName,
...props
},
ref,
@@ -67,6 +77,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
"flex h-[42px] w-auto rounded-l-md bg-white px-3 py-2 text-sm ",
"border items-center whitespace-nowrap",
props.disabled && "opacity-20",
prefixClassName,
)}
>
{customPrefix}
@@ -87,7 +98,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
ref={ref}
{...props}
className={cn(
inputVariants({ variant: error ? "error" : "default" }),
inputVariants({ variant: error ? "error" : variant }),
"flex h-[42px] w-full rounded-md bg-white px-3 py-2 text-sm file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-20 ",
"file:border-0",
"focus-visible:ring-2 focus-visible:ring-offset-2",
+114 -5
View File
@@ -7,22 +7,31 @@ import { ScrollArea } from "@components/ScrollArea";
import { AccessControlGroupCount } from "@components/ui/AccessControlGroupCount";
import GroupBadge from "@components/ui/GroupBadge";
import GroupBadgeWithEditPeers from "@components/ui/GroupBadgeWithEditPeers";
import TextWithTooltip from "@components/ui/TextWithTooltip";
import { VirtualScrollAreaList } from "@components/VirtualScrollAreaList";
import { useSearch } from "@hooks/useSearch";
import useSortedDropdownOptions from "@hooks/useSortedDropdownOptions";
import { IconArrowBack } from "@tabler/icons-react";
import useFetchApi from "@utils/api";
import { cn } from "@utils/helpers";
import { Command, CommandGroup, CommandInput, CommandList } from "cmdk";
import { sortBy, trim, unionBy } from "lodash";
import {
ChevronsUpDown,
FolderGit2,
GlobeIcon,
Layers3,
MonitorSmartphoneIcon,
NetworkIcon,
SearchIcon,
WorkflowIcon,
} from "lucide-react";
import * as React from "react";
import { useEffect, useMemo, useState } from "react";
import { Fragment, useEffect, useMemo, useState } from "react";
import { useGroups } from "@/contexts/GroupsProvider";
import { useElementSize } from "@/hooks/useElementSize";
import type { Group, GroupPeer } from "@/interfaces/Group";
import type {Group, GroupPeer, GroupResource} from "@/interfaces/Group";
import { NetworkResource } from "@/interfaces/Network";
import type { Peer } from "@/interfaces/Peer";
interface MultiSelectProps {
@@ -95,19 +104,21 @@ export function PeerGroupSelector({
const option = dropdownOptions.find((option) => option.name == name);
const groupPeers: GroupPeer[] | undefined =
(group?.peers as GroupPeer[]) || [];
const groupResources: GroupResource[] | undefined =
(group?.resources as GroupResource[]) || [];
if (peer) groupPeers?.push({ id: peer?.id as string, name: peer?.name });
if (!group && !option) {
addDropdownOptions([{ name: name, peers: groupPeers }]);
addDropdownOptions([{ name: name, peers: groupPeers, resources: groupResources }]);
}
if (max == 1 && values.length == 1) {
onChange([{ name: name, id: group?.id, peers: groupPeers }]);
onChange([{ name: name, id: group?.id, peers: groupPeers, resources: groupResources }]);
} else {
onChange((previous) => [
...previous,
{ name: name, id: group?.id, peers: groupPeers },
{ name: name, id: group?.id, peers: groupPeers, resources: groupResources },
]);
}
@@ -385,6 +396,8 @@ export function PeerGroupSelector({
<AccessControlGroupCount group_id={option.id} />
)}
<ResourcesCounter group={option} />
<div
className={
"text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-2"
@@ -407,3 +420,99 @@ export function PeerGroupSelector({
</Popover>
);
}
const ResourcesCounter = ({ group }: { group: Group }) => {
return group?.resources_count && group.resources_count > 0 ? (
<div
className={
"text-nb-gray-300 font-medium flex items-center gap-2 transition-all"
}
>
<Layers3 size={14} className={"shrink-0"} />
{group.resources_count} Resource(s)
</div>
) : null;
};
const resourcesSearchPredicate = (item: NetworkResource, query: string) => {
const lowerCaseQuery = query.toLowerCase();
if (item.name.toLowerCase().includes(lowerCaseQuery)) return true;
return item.address.toLowerCase().includes(lowerCaseQuery);
};
const ResourcesList = ({ search }: { search: string }) => {
const { data: resources, isLoading } = useFetchApi<NetworkResource[]>(
"/networks/resources",
);
const [filteredItems, _, setSearch] = useSearch(
resources || [],
resourcesSearchPredicate,
{ filter: true, debounce: 150 },
);
useEffect(() => {
setSearch(search);
}, [search, setSearch]);
return isLoading ? (
<>Loading...</>
) : (
filteredItems.length > 0 && (
<VirtualScrollAreaList
items={filteredItems}
onSelect={(option) => null}
renderItem={(res) => {
const isSelected = false;
return (
<Fragment key={res.id}>
<div className={"flex items-center gap-2"}>
<Badge
useHover={true}
data-cy={"group-badge"}
variant={"gray-ghost"}
className={cn("transition-all group whitespace-nowrap")}
onClick={(e) => {
e.preventDefault();
}}
>
{res.type === "host" && (
<WorkflowIcon
size={12}
className={"text-yellow-400 shrink-0"}
/>
)}
{res.type === "domain" && (
<GlobeIcon
size={12}
className={"text-yellow-400 shrink-0"}
/>
)}
{res.type === "subnet" && (
<NetworkIcon
size={12}
className={"text-yellow-400 shrink-0"}
/>
)}
<TextWithTooltip text={res?.name || ""} maxChars={20} />
</Badge>
</div>
<div className={"flex items-center gap-5"}>
<div
className={
"text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-2"
}
>
<Checkbox checked={isSelected} />
</div>
</div>
</Fragment>
);
}}
/>
)
);
};
+5 -2
View File
@@ -39,7 +39,7 @@ const Tabs = React.forwardRef<
Tabs.displayName = TabsPrimitive.Root.displayName;
type TabListProps = {
justify?: "start" | "end" | "center";
justify?: "start" | "end" | "center" | "between";
};
const TabsList = React.forwardRef<
@@ -54,6 +54,7 @@ const TabsList = React.forwardRef<
justify == "center" && "justify-center justify-items-end",
justify == "start" && "justify-start",
justify == "end" && "justify-end",
justify == "between" && "justify-between",
)}
{...props}
>
@@ -63,7 +64,9 @@ const TabsList = React.forwardRef<
}
/>
<ScrollArea>
<div className={"relative z-[1] flex flex-nowrap"}>{props.children}</div>
<div className={"relative z-[1] flex flex-nowrap w-full "}>
{props.children}
</div>
<ScrollBar orientation="horizontal" />
</ScrollArea>
</TabsPrimitive.List>
+26 -7
View File
@@ -11,17 +11,36 @@ type Props = {
onChange: (value: string) => void;
children: React.ReactNode;
};
const TabSwitchContext = React.createContext<{
switchTab: (value: string) => void;
}>({
switchTab: () => {},
});
export const useTabSwitchContext = () => {
return React.useContext(TabSwitchContext);
};
function VerticalTabs({ value, onChange, children }: Props) {
return (
<TabContext.Provider value={value || ""}>
<Tabs.Root
orientation={"vertical"}
className={"block lg:flex bg-nb-gray"}
value={value}
onValueChange={(value) => onChange(value)}
<TabSwitchContext.Provider
value={{
switchTab: (value: string) => {
onChange(value);
},
}}
>
{children}
</Tabs.Root>
<Tabs.Root
orientation={"vertical"}
className={"block lg:flex bg-nb-gray"}
value={value}
onValueChange={(value) => onChange(value)}
>
{children}
</Tabs.Root>
</TabSwitchContext.Provider>
</TabContext.Provider>
);
}
+12 -3
View File
@@ -11,6 +11,7 @@ interface Props extends IconVariant {
margin?: string;
truncate?: boolean;
children?: React.ReactNode;
center?: boolean;
}
export default function ModalHeader({
icon,
@@ -21,13 +22,21 @@ export default function ModalHeader({
margin = "mt-0",
truncate = false,
children,
center,
}: Props) {
return (
<div className={cn(className, "min-w-0")}>
<div className={"flex items-start gap-5 pr-10 min-w-0"}>
<div className={"flex items-start gap-5 min-w-0"}>
{icon && <SquareIcon color={color} icon={icon} />}
<div className={"min-w-0"}>
<h2 className={"text-lg my-0 leading-[1.5]"}>{title}</h2>
<div className={cn("min-w-0", center && "text-center")}>
<h2
className={cn(
"text-lg my-0 leading-[1.5]",
center && "text-center",
)}
>
{title}
</h2>
{children ? (
<>{children}</>
) : (
+2 -2
View File
@@ -101,11 +101,11 @@ const TableRow = React.forwardRef<
<tr
ref={ref}
className={cn(
" transition-colors data-[state=selected]:bg-neutral-100 dark:data-[state=selected]:bg-nb-gray-930/70",
" transition-colors group/table-row data-[state=selected]:bg-neutral-100 dark:data-[state=selected]:bg-nb-gray-930/70",
"dark:data-[state=selected]:border-nb-gray-900",
minimal
? "dark:hover:bg-nb-gray-900/10"
: "border-b dark:border-zinc-700/40 dark:hover:bg-nb-gray-900/20 hover:bg-neutral-100/50",
: "border-b dark:border-zinc-700/40 dark:hover:bg-nb-gray-940 hover:bg-neutral-100/50",
className,
)}
{...props}
+2 -2
View File
@@ -33,8 +33,8 @@ export default function UserDropdown() {
logout("/", { client_id: config.clientId }).then();
};
useHotkeys("shift+mod+l", () => logout(), []);
const { isOwnerOrAdmin, permission, isBillingAdmin } = useLoggedInUser();
useHotkeys("shift+mod+l", () => logoutSession(), []);
const { permission } = useLoggedInUser();
const [dropdownOpen, setDropdownOpen] = useState(false);
return (
+4 -2
View File
@@ -24,7 +24,7 @@ type DialogOptions = {
description?: string | React.ReactNode;
confirmText?: string;
cancelText?: string;
type?: "default" | "warning" | "danger";
type?: "default" | "warning" | "danger" | "center";
children?: React.ReactNode;
};
@@ -51,6 +51,7 @@ export default function DialogProvider({ children }: Props) {
default: "",
warning: <AlertCircle size={18} />,
danger: <AlertTriangle size={18} />,
center: "",
};
return (
@@ -61,8 +62,9 @@ export default function DialogProvider({ children }: Props) {
onOpenChange={(open) => fn.current && fn.current(open)}
>
{dialogOptions && (
<ModalContent maxWidthClass={"max-w-lg"} showClose={false}>
<ModalContent maxWidthClass={"max-w-[400px]"} showClose={false}>
<ModalHeader
center={dialogOptions.type == "center"}
title={dialogOptions.title || "Confirmation"}
margin={"mt-1"}
description={
Loaded 20 of 72 files, more files were not shown because too many files have changed in this diff. Show more