mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40902b3629 | ||
|
|
fa9bcea4ab | ||
|
|
3ba7acdecf | ||
|
|
c7775ade8c |
No files matched your search
+1
-2
@@ -12,6 +12,5 @@
|
|||||||
"tokenSource": "$NETBIRD_TOKEN_SOURCE",
|
"tokenSource": "$NETBIRD_TOKEN_SOURCE",
|
||||||
"dragQueryParams": "$NETBIRD_DRAG_QUERY_PARAMS",
|
"dragQueryParams": "$NETBIRD_DRAG_QUERY_PARAMS",
|
||||||
"hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID",
|
"hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID",
|
||||||
"googleAnalyticsID": "$NETBIRD_GOOGLE_ANALYTICS_ID",
|
"googleAnalyticsID": "$NETBIRD_GOOGLE_ANALYTICS_ID"
|
||||||
"authServiceUrl": "$NETBIRD_AUTH_SERVICE_URL"
|
|
||||||
}
|
}
|
||||||
Generated
+372
-195
File diff suppressed because it is too large.
Load diff
+3
-3
@@ -55,8 +55,8 @@
|
|||||||
"framer-motion": "^10.16.4",
|
"framer-motion": "^10.16.4",
|
||||||
"ip-cidr": "^3.1.0",
|
"ip-cidr": "^3.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.383.0",
|
"lucide-react": "^0.460.0",
|
||||||
"next": "13.5.5",
|
"next": "13.5.7",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"punycode": "^2.3.1",
|
"punycode": "^2.3.1",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cypress": "^13.3.3",
|
"cypress": "^13.13.0",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"prettier": "3.0.3",
|
"prettier": "3.0.3",
|
||||||
"tailwindcss": "^3"
|
"tailwindcss": "^3"
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import PeersProvider from "@/contexts/PeersProvider";
|
|||||||
import RoutesProvider from "@/contexts/RoutesProvider";
|
import RoutesProvider from "@/contexts/RoutesProvider";
|
||||||
import { Route } from "@/interfaces/Route";
|
import { Route } from "@/interfaces/Route";
|
||||||
import PageContainer from "@/layouts/PageContainer";
|
import PageContainer from "@/layouts/PageContainer";
|
||||||
|
import { NetworkRoutesDeprecationInfo } from "@/modules/networks/misc/NetworkRoutesDeprecationInfo";
|
||||||
import useGroupedRoutes from "@/modules/route-group/useGroupedRoutes";
|
import useGroupedRoutes from "@/modules/route-group/useGroupedRoutes";
|
||||||
|
|
||||||
const NetworkRoutesTable = lazy(
|
const NetworkRoutesTable = lazy(
|
||||||
@@ -39,7 +40,9 @@ export default function NetworkRoutes() {
|
|||||||
icon={<NetworkRoutesIcon size={13} />}
|
icon={<NetworkRoutesIcon size={13} />}
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
<h1 ref={headingRef}>Network Routes</h1>
|
<h1 ref={headingRef}>
|
||||||
|
Network Routes <NetworkRoutesDeprecationInfo size={18} />
|
||||||
|
</h1>
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
Network routes allow you to access other networks like LANs and
|
Network routes allow you to access other networks like LANs and
|
||||||
VPCs without installing NetBird on every resource.
|
VPCs without installing NetBird on every resource.
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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-concept"} 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -297,29 +297,12 @@ function PeerOverview() {
|
|||||||
/>
|
/>
|
||||||
</FullTooltip>
|
</FullTooltip>
|
||||||
|
|
||||||
<div>
|
{!isUser && (
|
||||||
<Label>Assigned Groups</Label>
|
<div>
|
||||||
<HelpText>
|
<Label>Assigned Groups</Label>
|
||||||
Use groups to control what this peer can access.
|
<HelpText>
|
||||||
</HelpText>
|
Use groups to control what this peer can access.
|
||||||
<FullTooltip
|
</HelpText>
|
||||||
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}
|
|
||||||
>
|
|
||||||
<PeerGroupSelector
|
<PeerGroupSelector
|
||||||
disabled={isUser}
|
disabled={isUser}
|
||||||
onChange={setSelectedGroups}
|
onChange={setSelectedGroups}
|
||||||
@@ -327,8 +310,8 @@ function PeerOverview() {
|
|||||||
hideAllGroup={true}
|
hideAllGroup={true}
|
||||||
peer={peer}
|
peer={peer}
|
||||||
/>
|
/>
|
||||||
</FullTooltip>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,17 +6,18 @@ import {
|
|||||||
AlertOctagonIcon,
|
AlertOctagonIcon,
|
||||||
FolderGit2Icon,
|
FolderGit2Icon,
|
||||||
LockIcon,
|
LockIcon,
|
||||||
|
NetworkIcon,
|
||||||
ShieldIcon,
|
ShieldIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||||
import { Role } from "@/interfaces/User";
|
|
||||||
import PageContainer from "@/layouts/PageContainer";
|
import PageContainer from "@/layouts/PageContainer";
|
||||||
import { useAccount } from "@/modules/account/useAccount";
|
import { useAccount } from "@/modules/account/useAccount";
|
||||||
import AuthenticationTab from "@/modules/settings/AuthenticationTab";
|
import AuthenticationTab from "@/modules/settings/AuthenticationTab";
|
||||||
import DangerZoneTab from "@/modules/settings/DangerZoneTab";
|
import DangerZoneTab from "@/modules/settings/DangerZoneTab";
|
||||||
import GroupsTab from "@/modules/settings/GroupsTab";
|
import GroupsTab from "@/modules/settings/GroupsTab";
|
||||||
|
import NetworkSettingsTab from "@/modules/settings/NetworkSettingsTab";
|
||||||
import PermissionsTab from "@/modules/settings/PermissionsTab";
|
import PermissionsTab from "@/modules/settings/PermissionsTab";
|
||||||
|
|
||||||
export default function NetBirdSettings() {
|
export default function NetBirdSettings() {
|
||||||
@@ -48,19 +49,21 @@ export default function NetBirdSettings() {
|
|||||||
<LockIcon size={14} />
|
<LockIcon size={14} />
|
||||||
Permissions
|
Permissions
|
||||||
</VerticalTabs.Trigger>
|
</VerticalTabs.Trigger>
|
||||||
|
<VerticalTabs.Trigger value="networks">
|
||||||
|
<NetworkIcon size={14} />
|
||||||
|
Networks
|
||||||
|
</VerticalTabs.Trigger>
|
||||||
<VerticalTabs.Trigger value="danger-zone" disabled={!isOwner}>
|
<VerticalTabs.Trigger value="danger-zone" disabled={!isOwner}>
|
||||||
<AlertOctagonIcon size={14} />
|
<AlertOctagonIcon size={14} />
|
||||||
Danger zone
|
Danger zone
|
||||||
</VerticalTabs.Trigger>
|
</VerticalTabs.Trigger>
|
||||||
</VerticalTabs.List>
|
</VerticalTabs.List>
|
||||||
<RestrictedAccess
|
<RestrictedAccess page={"Settings"}>
|
||||||
page={"Settings"}
|
|
||||||
allow={[Role.Admin, Role.Owner, Role.BillingAdmin]}
|
|
||||||
>
|
|
||||||
<div className={"border-l border-nb-gray-930 w-full"}>
|
<div className={"border-l border-nb-gray-930 w-full"}>
|
||||||
{account && <AuthenticationTab account={account} />}
|
{account && <AuthenticationTab account={account} />}
|
||||||
{account && <PermissionsTab account={account} />}
|
{account && <PermissionsTab account={account} />}
|
||||||
{account && <GroupsTab account={account} />}
|
{account && <GroupsTab account={account} />}
|
||||||
|
{account && <NetworkSettingsTab account={account} />}
|
||||||
{account && <DangerZoneTab account={account} />}
|
{account && <DangerZoneTab account={account} />}
|
||||||
</div>
|
</div>
|
||||||
</RestrictedAccess>
|
</RestrictedAccess>
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ export const OIDCError = () => {
|
|||||||
const params = useSearchParams();
|
const params = useSearchParams();
|
||||||
const errorParam = params.get("error");
|
const errorParam = params.get("error");
|
||||||
const accessDenied = errorParam === "access_denied";
|
const accessDenied = errorParam === "access_denied";
|
||||||
|
const invalidRequest = errorParam === "invalid_request";
|
||||||
const [title, setTitle] = useState(params.get("error_description"));
|
const [title, setTitle] = useState(params.get("error_description"));
|
||||||
|
const errorDescription = params.get("error_description");
|
||||||
const { logout, login } = useOidc();
|
const { logout, login } = useOidc();
|
||||||
|
|
||||||
useEffect(() => {
|
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 />
|
There was an error logging you in. <br />
|
||||||
Error: {oidcUserLoadingState}
|
Error:{" "}
|
||||||
|
<span className={"inline capitalize"}>
|
||||||
|
{invalidRequest && errorDescription
|
||||||
|
? errorDescription
|
||||||
|
: oidcUserLoadingState}
|
||||||
|
</span>
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Button
|
<Button
|
||||||
variant={"primary"}
|
variant={"primary"}
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ export const buttonVariants = cva(
|
|||||||
"dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20 ",
|
"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",
|
"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: [
|
input: [
|
||||||
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-neutral-200 text-gray-900",
|
"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 ",
|
"dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20 ",
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import FullTooltip from "@components/FullTooltip";
|
import FullTooltip from "@components/FullTooltip";
|
||||||
import Paragraph from "@components/Paragraph";
|
import Paragraph from "@components/Paragraph";
|
||||||
import { cn } from "@utils/helpers";
|
import { cn } from "@utils/helpers";
|
||||||
import { cva } from "class-variance-authority";
|
import { cva, VariantProps } from "class-variance-authority";
|
||||||
import { AlertCircle } from "lucide-react";
|
import { AlertCircle } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
|
type InputVariants = VariantProps<typeof inputVariants>;
|
||||||
|
|
||||||
export interface InputProps
|
export interface InputProps
|
||||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
extends React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
InputVariants {
|
||||||
customPrefix?: React.ReactNode;
|
customPrefix?: React.ReactNode;
|
||||||
customSuffix?: React.ReactNode;
|
customSuffix?: React.ReactNode;
|
||||||
maxWidthClass?: string;
|
maxWidthClass?: string;
|
||||||
@@ -14,6 +17,7 @@ export interface InputProps
|
|||||||
error?: string;
|
error?: string;
|
||||||
errorTooltip?: boolean;
|
errorTooltip?: boolean;
|
||||||
errorTooltipPosition?: "top" | "top-right";
|
errorTooltipPosition?: "top" | "top-right";
|
||||||
|
prefixClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputVariants = cva("", {
|
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",
|
"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",
|
"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: [
|
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",
|
"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",
|
"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,
|
error,
|
||||||
errorTooltip = false,
|
errorTooltip = false,
|
||||||
errorTooltipPosition = "top",
|
errorTooltipPosition = "top",
|
||||||
|
variant = "default",
|
||||||
|
prefixClassName,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref,
|
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 ",
|
"flex h-[42px] w-auto rounded-l-md bg-white px-3 py-2 text-sm ",
|
||||||
"border items-center whitespace-nowrap",
|
"border items-center whitespace-nowrap",
|
||||||
props.disabled && "opacity-20",
|
props.disabled && "opacity-20",
|
||||||
|
prefixClassName,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{customPrefix}
|
{customPrefix}
|
||||||
@@ -87,7 +98,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
className={cn(
|
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 ",
|
"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",
|
"file:border-0",
|
||||||
"focus-visible:ring-2 focus-visible:ring-offset-2",
|
"focus-visible:ring-2 focus-visible:ring-offset-2",
|
||||||
|
|||||||
@@ -7,22 +7,31 @@ import { ScrollArea } from "@components/ScrollArea";
|
|||||||
import { AccessControlGroupCount } from "@components/ui/AccessControlGroupCount";
|
import { AccessControlGroupCount } from "@components/ui/AccessControlGroupCount";
|
||||||
import GroupBadge from "@components/ui/GroupBadge";
|
import GroupBadge from "@components/ui/GroupBadge";
|
||||||
import GroupBadgeWithEditPeers from "@components/ui/GroupBadgeWithEditPeers";
|
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 useSortedDropdownOptions from "@hooks/useSortedDropdownOptions";
|
||||||
import { IconArrowBack } from "@tabler/icons-react";
|
import { IconArrowBack } from "@tabler/icons-react";
|
||||||
|
import useFetchApi from "@utils/api";
|
||||||
import { cn } from "@utils/helpers";
|
import { cn } from "@utils/helpers";
|
||||||
import { Command, CommandGroup, CommandInput, CommandList } from "cmdk";
|
import { Command, CommandGroup, CommandInput, CommandList } from "cmdk";
|
||||||
import { sortBy, trim, unionBy } from "lodash";
|
import { sortBy, trim, unionBy } from "lodash";
|
||||||
import {
|
import {
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
FolderGit2,
|
FolderGit2,
|
||||||
|
GlobeIcon,
|
||||||
|
Layers3,
|
||||||
MonitorSmartphoneIcon,
|
MonitorSmartphoneIcon,
|
||||||
|
NetworkIcon,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
|
WorkflowIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import * as React from "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 { useGroups } from "@/contexts/GroupsProvider";
|
||||||
import { useElementSize } from "@/hooks/useElementSize";
|
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";
|
import type { Peer } from "@/interfaces/Peer";
|
||||||
|
|
||||||
interface MultiSelectProps {
|
interface MultiSelectProps {
|
||||||
@@ -95,19 +104,21 @@ export function PeerGroupSelector({
|
|||||||
const option = dropdownOptions.find((option) => option.name == name);
|
const option = dropdownOptions.find((option) => option.name == name);
|
||||||
const groupPeers: GroupPeer[] | undefined =
|
const groupPeers: GroupPeer[] | undefined =
|
||||||
(group?.peers as GroupPeer[]) || [];
|
(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 (peer) groupPeers?.push({ id: peer?.id as string, name: peer?.name });
|
||||||
|
|
||||||
if (!group && !option) {
|
if (!group && !option) {
|
||||||
addDropdownOptions([{ name: name, peers: groupPeers }]);
|
addDropdownOptions([{ name: name, peers: groupPeers, resources: groupResources }]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (max == 1 && values.length == 1) {
|
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 {
|
} else {
|
||||||
onChange((previous) => [
|
onChange((previous) => [
|
||||||
...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} />
|
<AccessControlGroupCount group_id={option.id} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<ResourcesCounter group={option} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
"text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-2"
|
"text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-2"
|
||||||
@@ -407,3 +420,99 @@ export function PeerGroupSelector({
|
|||||||
</Popover>
|
</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>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -39,7 +39,7 @@ const Tabs = React.forwardRef<
|
|||||||
Tabs.displayName = TabsPrimitive.Root.displayName;
|
Tabs.displayName = TabsPrimitive.Root.displayName;
|
||||||
|
|
||||||
type TabListProps = {
|
type TabListProps = {
|
||||||
justify?: "start" | "end" | "center";
|
justify?: "start" | "end" | "center" | "between";
|
||||||
};
|
};
|
||||||
|
|
||||||
const TabsList = React.forwardRef<
|
const TabsList = React.forwardRef<
|
||||||
@@ -54,6 +54,7 @@ const TabsList = React.forwardRef<
|
|||||||
justify == "center" && "justify-center justify-items-end",
|
justify == "center" && "justify-center justify-items-end",
|
||||||
justify == "start" && "justify-start",
|
justify == "start" && "justify-start",
|
||||||
justify == "end" && "justify-end",
|
justify == "end" && "justify-end",
|
||||||
|
justify == "between" && "justify-between",
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@@ -63,7 +64,9 @@ const TabsList = React.forwardRef<
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ScrollArea>
|
<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" />
|
<ScrollBar orientation="horizontal" />
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</TabsPrimitive.List>
|
</TabsPrimitive.List>
|
||||||
|
|||||||
@@ -11,17 +11,36 @@ type Props = {
|
|||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
children: React.ReactNode;
|
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) {
|
function VerticalTabs({ value, onChange, children }: Props) {
|
||||||
return (
|
return (
|
||||||
<TabContext.Provider value={value || ""}>
|
<TabContext.Provider value={value || ""}>
|
||||||
<Tabs.Root
|
<TabSwitchContext.Provider
|
||||||
orientation={"vertical"}
|
value={{
|
||||||
className={"block lg:flex bg-nb-gray"}
|
switchTab: (value: string) => {
|
||||||
value={value}
|
onChange(value);
|
||||||
onValueChange={(value) => 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>
|
</TabContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface Props extends IconVariant {
|
|||||||
margin?: string;
|
margin?: string;
|
||||||
truncate?: boolean;
|
truncate?: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
center?: boolean;
|
||||||
}
|
}
|
||||||
export default function ModalHeader({
|
export default function ModalHeader({
|
||||||
icon,
|
icon,
|
||||||
@@ -21,13 +22,21 @@ export default function ModalHeader({
|
|||||||
margin = "mt-0",
|
margin = "mt-0",
|
||||||
truncate = false,
|
truncate = false,
|
||||||
children,
|
children,
|
||||||
|
center,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
return (
|
return (
|
||||||
<div className={cn(className, "min-w-0")}>
|
<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} />}
|
{icon && <SquareIcon color={color} icon={icon} />}
|
||||||
<div className={"min-w-0"}>
|
<div className={cn("min-w-0", center && "text-center")}>
|
||||||
<h2 className={"text-lg my-0 leading-[1.5]"}>{title}</h2>
|
<h2
|
||||||
|
className={cn(
|
||||||
|
"text-lg my-0 leading-[1.5]",
|
||||||
|
center && "text-center",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
{children ? (
|
{children ? (
|
||||||
<>{children}</>
|
<>{children}</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -101,11 +101,11 @@ const TableRow = React.forwardRef<
|
|||||||
<tr
|
<tr
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
"dark:data-[state=selected]:border-nb-gray-900",
|
||||||
minimal
|
minimal
|
||||||
? "dark:hover:bg-nb-gray-900/10"
|
? "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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ export default function UserDropdown() {
|
|||||||
logout("/", { client_id: config.clientId }).then();
|
logout("/", { client_id: config.clientId }).then();
|
||||||
};
|
};
|
||||||
|
|
||||||
useHotkeys("shift+mod+l", () => logout(), []);
|
useHotkeys("shift+mod+l", () => logoutSession(), []);
|
||||||
const { isOwnerOrAdmin, permission, isBillingAdmin } = useLoggedInUser();
|
const { permission } = useLoggedInUser();
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ type DialogOptions = {
|
|||||||
description?: string | React.ReactNode;
|
description?: string | React.ReactNode;
|
||||||
confirmText?: string;
|
confirmText?: string;
|
||||||
cancelText?: string;
|
cancelText?: string;
|
||||||
type?: "default" | "warning" | "danger";
|
type?: "default" | "warning" | "danger" | "center";
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,6 +51,7 @@ export default function DialogProvider({ children }: Props) {
|
|||||||
default: "",
|
default: "",
|
||||||
warning: <AlertCircle size={18} />,
|
warning: <AlertCircle size={18} />,
|
||||||
danger: <AlertTriangle size={18} />,
|
danger: <AlertTriangle size={18} />,
|
||||||
|
center: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -61,8 +62,9 @@ export default function DialogProvider({ children }: Props) {
|
|||||||
onOpenChange={(open) => fn.current && fn.current(open)}
|
onOpenChange={(open) => fn.current && fn.current(open)}
|
||||||
>
|
>
|
||||||
{dialogOptions && (
|
{dialogOptions && (
|
||||||
<ModalContent maxWidthClass={"max-w-lg"} showClose={false}>
|
<ModalContent maxWidthClass={"max-w-[400px]"} showClose={false}>
|
||||||
<ModalHeader
|
<ModalHeader
|
||||||
|
center={dialogOptions.type == "center"}
|
||||||
title={dialogOptions.title || "Confirmation"}
|
title={dialogOptions.title || "Confirmation"}
|
||||||
margin={"mt-1"}
|
margin={"mt-1"}
|
||||||
description={
|
description={
|
||||||
|
|||||||
Loaded 20 of 72 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user