mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0cca1aa31 | ||
|
|
6a26f546c5 | ||
|
|
5776a7dd5c | ||
|
|
208176ba9d | ||
|
|
8a1ba01ef2 | ||
|
|
e8df6c153c | ||
|
|
8023ff5047 | ||
|
|
32ecc374bb | ||
|
|
f3a4f22e1e | ||
|
|
968ca333c1 | ||
|
|
1e77129bdf | ||
|
|
0dfdaf62c6 | ||
|
|
2f74430616 | ||
|
|
b3cf73950e | ||
|
|
addea360a1 | ||
|
|
4a597a7fdd | ||
|
|
ba88b838f9 | ||
|
|
a8275e54d5 | ||
|
|
f77d0dc48e | ||
|
|
67cd3ddf08 | ||
|
|
d90fe688f5 | ||
|
|
6dd3dfa153 | ||
|
|
68128dffb7 | ||
|
|
daff0659d4 | ||
|
|
019320a481 | ||
|
|
d1a2d407b3 | ||
|
|
41043bb10c | ||
|
|
d4f6adb807 | ||
|
|
ccf417e103 | ||
|
|
6a6a3b86c4 | ||
|
|
9849e0a70c | ||
|
|
9249464d5c | ||
|
|
06daecf89a | ||
|
|
d58d50c4ec | ||
|
|
97007db62e |
No files matched your search
+6
-6
@@ -35,6 +35,9 @@ yarn-error.log*
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# local UI sandbox (throwaway preview harness — never commit)
|
||||
/src/app/sandbox/
|
||||
|
||||
# config
|
||||
.local*config*.json
|
||||
.test-config.json
|
||||
@@ -51,11 +54,8 @@ playwright-report*.zip
|
||||
.configs
|
||||
|
||||
/public/ironrdp-pkg/
|
||||
/public/netbird.wasm*
|
||||
/public/netbird.wasm
|
||||
.idea
|
||||
src/.local-config*
|
||||
.deepsec/
|
||||
|
||||
|
||||
# Local SDK tarballs from the agent repo (packed by scripts, not committed)
|
||||
vendor/
|
||||
# local mock data (throwaway preview — never commit)
|
||||
/src/utils/mockData.ts
|
||||
+1
-2
@@ -24,6 +24,5 @@
|
||||
"hubspotSignupFormId": "$NETBIRD_HUBSPOT_SIGNUP_FORM_ID",
|
||||
"hubspotOnboardingFormId": "$NETBIRD_HUBSPOT_ONBOARDING_FORM_ID",
|
||||
"hubspotSurveyFormId": "$NETBIRD_HUBSPOT_SURVEY_FORM_ID",
|
||||
"analyticsExcludedEmails": "$NETBIRD_ANALYTICS_EXCLUDED_EMAILS",
|
||||
"assistantApiOrigin": "$NETBIRD_ASSISTANT_API_ENDPOINT"
|
||||
"analyticsExcludedEmails": "$NETBIRD_ANALYTICS_EXCLUDED_EMAILS"
|
||||
}
|
||||
@@ -73,8 +73,6 @@ export NETBIRD_HUBSPOT_SIGNUP_FORM_ID=${NETBIRD_HUBSPOT_SIGNUP_FORM_ID}
|
||||
export NETBIRD_HUBSPOT_ONBOARDING_FORM_ID=${NETBIRD_HUBSPOT_ONBOARDING_FORM_ID}
|
||||
export NETBIRD_HUBSPOT_SURVEY_FORM_ID=${NETBIRD_HUBSPOT_SURVEY_FORM_ID}
|
||||
export NETBIRD_ANALYTICS_EXCLUDED_EMAILS=${NETBIRD_ANALYTICS_EXCLUDED_EMAILS}
|
||||
# No default: an unset endpoint is what turns the assistant off.
|
||||
export NETBIRD_ASSISTANT_API_ENDPOINT=${NETBIRD_ASSISTANT_API_ENDPOINT}
|
||||
|
||||
echo "NetBird latest version: ${NETBIRD_LATEST_VERSION}"
|
||||
|
||||
@@ -123,12 +121,6 @@ if [[ -n "${NETBIRD_MGMT_API_ENDPOINT}" ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Add NETBIRD_ASSISTANT_API_ENDPOINT to CSP — the browser streams the chat
|
||||
# straight from this origin, so without it connect-src blocks every request.
|
||||
if [[ -n "${NETBIRD_ASSISTANT_API_ENDPOINT}" ]]; then
|
||||
CSP_DOMAINS="$CSP_DOMAINS $NETBIRD_ASSISTANT_API_ENDPOINT"
|
||||
fi
|
||||
|
||||
# Add LETSENCRYPT_DOMAIN to CSP
|
||||
if [[ -n "${LETSENCRYPT_DOMAIN}" ]]; then
|
||||
if [[ "$LETSENCRYPT_DOMAIN" == *"localhost"* ]]; then
|
||||
@@ -171,7 +163,7 @@ sed -i "s|add_header Content-Security-Policy \"[^\"]*\" always;|$CSP_HEADER|g" /
|
||||
}
|
||||
|
||||
# replace ENVs in the config
|
||||
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_CLIENT_SECRET \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$NETBIRD_GOOGLE_ANALYTICS_ID \$\$NETBIRD_GOOGLE_TAG_MANAGER_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE \$\$NETBIRD_DRAG_QUERY_PARAMS \$\$NETBIRD_AUTH_SERVICE_URL \$\$NETBIRD_WASM_PATH \$\$NETBIRD_LICENSED \$\$NETBIRD_CLOUD \$\$NETBIRD_AGENT_NETWORK_ONLY \$\$NETBIRD_AGENT_NETWORK_ENABLED \$\$NETBIRD_HUBSPOT_PORTAL_ID \$\$NETBIRD_HUBSPOT_SIGNUP_FORM_ID \$\$NETBIRD_HUBSPOT_ONBOARDING_FORM_ID \$\$NETBIRD_HUBSPOT_SURVEY_FORM_ID \$\$NETBIRD_ANALYTICS_EXCLUDED_EMAILS \$\$NETBIRD_ASSISTANT_API_ENDPOINT"
|
||||
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_CLIENT_SECRET \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$NETBIRD_GOOGLE_ANALYTICS_ID \$\$NETBIRD_GOOGLE_TAG_MANAGER_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE \$\$NETBIRD_DRAG_QUERY_PARAMS \$\$NETBIRD_AUTH_SERVICE_URL \$\$NETBIRD_WASM_PATH \$\$NETBIRD_LICENSED \$\$NETBIRD_CLOUD \$\$NETBIRD_AGENT_NETWORK_ONLY \$\$NETBIRD_AGENT_NETWORK_ENABLED \$\$NETBIRD_HUBSPOT_PORTAL_ID \$\$NETBIRD_HUBSPOT_SIGNUP_FORM_ID \$\$NETBIRD_HUBSPOT_ONBOARDING_FORM_ID \$\$NETBIRD_HUBSPOT_SURVEY_FORM_ID \$\$NETBIRD_ANALYTICS_EXCLUDED_EMAILS"
|
||||
|
||||
OIDC_TRUSTED_DOMAINS="/usr/share/nginx/html/OidcTrustedDomains.js"
|
||||
envsubst "$ENV_STR" < "$OIDC_TRUSTED_DOMAINS".tmpl > "$OIDC_TRUSTED_DOMAINS"
|
||||
|
||||
@@ -74,7 +74,7 @@ export async function resetDraftState(page: Page) {
|
||||
export async function readDraftChanges(page: Page): Promise<any[]> {
|
||||
return await page.evaluate(
|
||||
() =>
|
||||
(window as unknown as { __controlCenterDraftChanges?: any[] }).__controlCenterDraftChanges ??
|
||||
(window as unknown as { __ccDraftChanges?: any[] }).__ccDraftChanges ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
@@ -212,7 +212,7 @@ export async function createViaCanvasMenu(
|
||||
export async function readDraftCanvas(page: Page): Promise<any | null> {
|
||||
return await page.evaluate(
|
||||
() =>
|
||||
(window as unknown as { __controlCenterDraftCanvas?: unknown }).__controlCenterDraftCanvas ??
|
||||
(window as unknown as { __ccDraftCanvas?: unknown }).__ccDraftCanvas ??
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+4
-20
@@ -1,11 +1,3 @@
|
||||
/**
|
||||
* ESLint flat config.
|
||||
*
|
||||
* Replaces `.eslintrc.json`, which ESLint 9 no longer reads by default, and
|
||||
* `next lint`, which Next 16 removed. `eslint-config-next` v16 already exports
|
||||
* flat config arrays, so this is a direct translation of the old eslintrc rather
|
||||
* than a FlatCompat shim.
|
||||
*/
|
||||
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
|
||||
import prettier from "eslint-config-prettier";
|
||||
import simpleImportSort from "eslint-plugin-simple-import-sort";
|
||||
@@ -13,31 +5,23 @@ import simpleImportSort from "eslint-plugin-simple-import-sort";
|
||||
const config = [
|
||||
{
|
||||
ignores: [
|
||||
"node_modules/**",
|
||||
".next/**",
|
||||
".claude/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"public/**",
|
||||
"playwright-report/**",
|
||||
"test-logs/**",
|
||||
"next-env.d.ts",
|
||||
"public/**",
|
||||
],
|
||||
},
|
||||
|
||||
...nextCoreWebVitals,
|
||||
|
||||
// Must come after the Next config so it can switch off stylistic rules that
|
||||
// would otherwise fight Prettier.
|
||||
prettier,
|
||||
|
||||
{
|
||||
files: ["**/*.{js,jsx,mjs,ts,tsx,mts,cts}"],
|
||||
plugins: { "simple-import-sort": simpleImportSort },
|
||||
rules: {
|
||||
"simple-import-sort/imports": [
|
||||
"warn",
|
||||
{ groups: [["^\\u0000", "^@?\\w", "^[^.]", "^\\."]] },
|
||||
{
|
||||
groups: [["^\\u0000", "^@?\\w", "^[^.]", "^\\."]],
|
||||
},
|
||||
],
|
||||
"simple-import-sort/exports": "warn",
|
||||
},
|
||||
|
||||
@@ -10,10 +10,6 @@ const nextConfig = {
|
||||
NEXT_PUBLIC_DASHBOARD_VERSION:
|
||||
process.env.NEXT_PUBLIC_DASHBOARD_VERSION || "development",
|
||||
},
|
||||
// The assistant SDKs are consumed as packed tarballs under ./vendor, so
|
||||
// they live inside node_modules like any other dependency; they ship
|
||||
// prebuilt ESM with "use client" banners, which Next consumes directly.
|
||||
transpilePackages: ["@netbird/assistant-react"],
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
Generated
+525
-2695
File diff suppressed because it is too large.
Load diff
@@ -23,11 +23,8 @@
|
||||
"test:ci": "cross-env APP_ENV=test next build && npx playwright test --config=e2e/playwright.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@assistant-ui/react": "^0.15.8",
|
||||
"@assistant-ui/react-markdown": "^0.14.9",
|
||||
"@axa-fr/react-oidc": "^7.26.3",
|
||||
"@dagrejs/dagre": "^1.1.5",
|
||||
"@netbird/assistant-react": "file:vendor/netbird-assistant-react-0.1.0.tgz",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
@@ -74,11 +71,9 @@
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"framer-motion": "^12.29.2",
|
||||
"highlight.js": "^11.11.1",
|
||||
"ip-address": "^10.2.0",
|
||||
"js-cookie": "^3.0.7",
|
||||
"lodash": "4.18.1",
|
||||
"lowlight": "^3.3.0",
|
||||
"lucide-react": "^0.566.0",
|
||||
"next": "16.3.4",
|
||||
"punycode": "^2.3.1",
|
||||
@@ -94,7 +89,6 @@
|
||||
"react-loading-skeleton": "^3.3.1",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react-virtuoso": "^4.9.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sonner": "^2.0.7",
|
||||
"swr": "^2.2.4",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function Integrations() {
|
||||
(!isNetBirdCloud() && (permission?.settings?.read ?? false))
|
||||
}
|
||||
>
|
||||
<div className={"border-l border-nb-gray-930 w-full"}>
|
||||
<div className={"w-full"}>
|
||||
<IdentityProviderTab />
|
||||
<SSOTab />
|
||||
<EventStreamingTab />
|
||||
|
||||
@@ -87,7 +87,7 @@ export default function AgentNetworkConfigurationPage() {
|
||||
<GroupsProvider>
|
||||
<PeersProvider>
|
||||
<AIProvidersProvider>
|
||||
<div className={"border-l border-nb-gray-930 w-full"}>
|
||||
<div className={"w-full"}>
|
||||
<Tabs.Content
|
||||
value={TAB_BUDGET_SETTINGS}
|
||||
className={"w-full"}
|
||||
|
||||
@@ -15,11 +15,15 @@ import React, { useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import GroupsProvider from "@/contexts/GroupsProvider";
|
||||
import PeersProvider from "@/contexts/PeersProvider";
|
||||
import { useTheme } from "@/contexts/ThemeProvider";
|
||||
import {
|
||||
canvasBackground,
|
||||
canvasDotColor,
|
||||
} from "@/modules/control-center/utils/canvas-theme";
|
||||
import PoliciesProvider from "@/contexts/PoliciesProvider";
|
||||
import { Network } from "@/interfaces/Network";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
import AIProvidersProvider from "@/modules/agent-network/AIProvidersProvider";
|
||||
import { CanvasAgentBridge } from "@/modules/control-center/agent/CanvasAgentBridge";
|
||||
import {
|
||||
CanvasStateProvider,
|
||||
ControlCenterUIProvider,
|
||||
@@ -49,7 +53,6 @@ import { PeersToolbar } from "@/modules/control-center/draft/PeersToolbar";
|
||||
import { ConnectionLine } from "@/modules/control-center/edges/ConnectionLine";
|
||||
import { ControlCenterEmptyStates } from "@/modules/control-center/header/ControlCenterEmptyStates";
|
||||
import { ControlCenterHeader } from "@/modules/control-center/header/ControlCenterHeader";
|
||||
import { useAssistantPanelPan } from "@/modules/control-center/hooks/useAssistantPanelPan";
|
||||
import { useDraft } from "@/modules/control-center/hooks/useDraft";
|
||||
import { useDragToGroup } from "@/modules/control-center/hooks/useDragToGroup";
|
||||
import { useDrillDownBrowserHistory } from "@/modules/control-center/hooks/useDrillDownBrowserHistory";
|
||||
@@ -107,6 +110,8 @@ const PRO_OPTIONS = { hideAttribution: true };
|
||||
const DEFAULT_VIEWPORT = { x: 0, y: 0, zoom: EMPTY_STATE_ZOOM };
|
||||
|
||||
function ControlCenterCanvas() {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const isLight = resolvedTheme === "light";
|
||||
const canvas = useCanvasState();
|
||||
const ui = useControlCenterUI();
|
||||
const draft = useDraft();
|
||||
@@ -118,7 +123,6 @@ function ControlCenterCanvas() {
|
||||
const { onNodeDragStart, onNodeDrag, onNodeDragStop } = useDragToGroup();
|
||||
useDrillDownBrowserHistory();
|
||||
useGroupFocusDim();
|
||||
useAssistantPanelPan();
|
||||
|
||||
const { mutate } = useSWRConfig();
|
||||
const onLiveNetworkCreated = React.useCallback(
|
||||
@@ -256,8 +260,6 @@ function ControlCenterCanvas() {
|
||||
<DraftNetworkDestinationModal />
|
||||
<DraftNetworkEditModal />
|
||||
<DraftLeaveGuard />
|
||||
{/* Publishes the canvas to the assistant for as long as this page is up. */}
|
||||
<CanvasAgentBridge onConnect={draft.onNodeConnect} />
|
||||
<ReactFlow
|
||||
className={highlightArmed ? "cc-focus-armed" : undefined}
|
||||
edges={canvas.edges}
|
||||
@@ -288,7 +290,7 @@ function ControlCenterCanvas() {
|
||||
onInit={stableOnInit}
|
||||
maxZoom={DEFAULT_MAX_ZOOM}
|
||||
minZoom={DEFAULT_MIN_ZOOM}
|
||||
colorMode={"dark"}
|
||||
colorMode={isLight ? "light" : "dark"}
|
||||
panOnDrag={canInteract}
|
||||
panOnScroll={draft.isSelectMode && !emptyState}
|
||||
zoomOnScroll={canInteract}
|
||||
@@ -302,7 +304,11 @@ function ControlCenterCanvas() {
|
||||
selectionOnDrag={draft.isSelectMode && !emptyState}
|
||||
selectionMode={SelectionMode.Partial}
|
||||
>
|
||||
<Background bgColor={"#181a1d"} gap={20} color={"#717171"} />
|
||||
<Background
|
||||
bgColor={canvasBackground(resolvedTheme)}
|
||||
gap={20}
|
||||
color={canvasDotColor(resolvedTheme)}
|
||||
/>
|
||||
<CanvasContextMenu onOpenChange={setContextMenuOpen} />
|
||||
<NodeContextMenu
|
||||
position={nodeContextMenuPos}
|
||||
|
||||
@@ -115,7 +115,7 @@ const GroupDetailsName = () => {
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-8 w-8 items-center justify-center gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 rounded-md cursor-pointer",
|
||||
"flex h-8 w-8 items-center justify-center gap-2 text-nb-gray-300 hover:text-nb-gray-100 transition-all hover:bg-nb-gray-800/60 rounded-md cursor-pointer",
|
||||
!isAllowedToRename &&
|
||||
"opacity-40 cursor-not-allowed pointer-events-none",
|
||||
)}
|
||||
|
||||
@@ -257,7 +257,7 @@ const PeerHeader = () => {
|
||||
<ModalTrigger>
|
||||
<div
|
||||
className={
|
||||
"flex h-8 w-8 items-center justify-center gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 rounded-md cursor-pointer"
|
||||
"flex h-8 w-8 items-center justify-center gap-2 text-nb-gray-400 hover:text-nb-gray-100 transition-all hover:bg-nb-gray-800/60 rounded-md cursor-pointer"
|
||||
}
|
||||
>
|
||||
<PencilIcon size={16} />
|
||||
@@ -741,7 +741,7 @@ function EditableValue({
|
||||
<span>{value}</span>
|
||||
{canEdit && (
|
||||
<button
|
||||
className="flex w-7 h-7 items-center justify-center gap-2 text-nb-gray-400 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 rounded-md cursor-pointer"
|
||||
className="flex w-7 h-7 items-center justify-center gap-2 text-nb-gray-400 hover:text-nb-gray-100 transition-all hover:bg-nb-gray-800/60 rounded-md cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit();
|
||||
|
||||
@@ -107,7 +107,7 @@ export default function NetBirdSettings() {
|
||||
page={"Settings"}
|
||||
hasAccess={permission?.billing?.read || permission?.settings?.read}
|
||||
>
|
||||
<div className={"border-l border-nb-gray-930 w-full"}>
|
||||
<div className={"w-full"}>
|
||||
{account && <AuthenticationTab account={account} />}
|
||||
{permission.setup_keys.read && <SetupKeysTab />}
|
||||
{account?.settings?.embedded_idp_enabled &&
|
||||
|
||||
@@ -183,7 +183,7 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
|
||||
<div className={"flex items-center gap-3"}>
|
||||
<div
|
||||
className={
|
||||
"w-10 h-10 rounded-full relative flex items-center justify-center text-white uppercase text-md font-medium bg-nb-gray-900"
|
||||
"w-10 h-10 rounded-full relative flex items-center justify-center text-nb-gray-100 uppercase text-md font-medium bg-nb-gray-900"
|
||||
}
|
||||
style={
|
||||
isServiceUser
|
||||
|
||||
@@ -1,283 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useOidc } from "@axa-fr/react-oidc";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import { CheckCircle2Icon, CircleXIcon, Loader2Icon } from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
ACCESS_GRANTED_MESSAGE,
|
||||
type AccessGrantedMessage,
|
||||
consumeAccessNonce,
|
||||
hasAccessNonce,
|
||||
isAllowedAccessRule,
|
||||
} from "@/modules/assistant/assistantAccessAuth";
|
||||
|
||||
/**
|
||||
* Authorizes the assistant to reach one peer, in a window the user opened.
|
||||
*
|
||||
* ## Why this is a page and not a fetch
|
||||
*
|
||||
* Everything here could be done by the assistant panel directly — it has the
|
||||
* user's bearer already. The point is that it is NOT: the grant is created by a
|
||||
* window a person opened and signed in to. A silent token refresh proves a
|
||||
* refresh token exists in storage; this proves someone is at the keyboard.
|
||||
*
|
||||
* ## Why a sign-in is not enough on its own
|
||||
*
|
||||
* Everything below arrives in the URL, and a page that turns URL parameters
|
||||
* into SSH access is a page anyone can write a link to. The sign-in proves a
|
||||
* person is present; it does not prove they meant to grant THIS key access to
|
||||
* THIS machine, and a link mailed to an admin looks from here exactly like a
|
||||
* request the assistant made — same origin, same familiar sign-in.
|
||||
*
|
||||
* So two things are checked before anything is granted. The nonce the opener
|
||||
* left in `sessionStorage`, which a window opened from the panel inherits and
|
||||
* a window opened from a link does not; and the rules, against the set this
|
||||
* flow is allowed to ask for, since `tcp/3389` would be RDP rather than SSH.
|
||||
*
|
||||
* ## Why it forces a fresh sign-in
|
||||
*
|
||||
* Sitting behind the authenticated layout is not enough, and this is the part
|
||||
* that is easy to get wrong. With a live dashboard session the guard
|
||||
* authenticates silently, the window flashes "granted", and nobody has proved
|
||||
* anything — the check degrades to "a window was opened", which a background
|
||||
* tab could have done. `prompt=login` makes the identity provider ask again,
|
||||
* every time, which is the whole reason this window exists.
|
||||
*
|
||||
* ## What it grants
|
||||
*
|
||||
* Exactly what the opener asked for, for one peer: a temporary-access peer
|
||||
* carrying the assistant tab's OWN WireGuard public key, so the grant lands on
|
||||
* the peer the assistant is already running rather than on a second one that
|
||||
* would die with this window. The key arrives in the URL and is public by
|
||||
* definition — the private half stays in the assistant's tab and is never part
|
||||
* of this flow.
|
||||
*/
|
||||
|
||||
/** Marks the return trip, so the forced sign-in happens once and not in a loop. */
|
||||
const REAUTH_MARKER = "reauth";
|
||||
|
||||
/**
|
||||
* How long the success state stays up before the window closes itself.
|
||||
*
|
||||
* Not zero. This window is the only place the grant is confirmed and the only
|
||||
* place the machine it covers is named, and one that vanishes the instant it
|
||||
* appears reads as one that failed. The beat also keeps the reply ahead of the
|
||||
* close: the grant is reported to the opener by `postMessage`, and leaving a
|
||||
* gap means nothing here depends on how a browser orders a queued message
|
||||
* against a window that is going away.
|
||||
*/
|
||||
const AUTO_CLOSE_MS = 1_200;
|
||||
|
||||
/**
|
||||
* How long after the close attempt to conclude the browser refused it.
|
||||
*
|
||||
* A script-opened window may normally close itself, but that is a permission
|
||||
* and not a guarantee. If this timer still runs, the close did not happen and
|
||||
* the person is looking at a window with no way out of it.
|
||||
*/
|
||||
const CLOSE_REFUSED_MS = 400;
|
||||
|
||||
export default function AssistantAccessPage() {
|
||||
const params = useSearchParams();
|
||||
const { login } = useOidc();
|
||||
const peerRequest = useApiCall("/peers");
|
||||
const [state, setState] = useState<"working" | "granted" | "failed">(
|
||||
"working",
|
||||
);
|
||||
const [error, setError] = useState("");
|
||||
const [closeRefused, setCloseRefused] = useState(false);
|
||||
// React mounts effects twice in dev; a double grant would create a second
|
||||
// policy for the same peer, and a double login would bounce the window.
|
||||
const started = useRef(false);
|
||||
|
||||
const peerId = params.get("peer");
|
||||
const peerLabel = params.get("label") ?? "this peer";
|
||||
const wgPublicKey = params.get("key");
|
||||
const assistantPeerName = params.get("name");
|
||||
const rules = (params.get("rules") ?? "").split(",").filter(Boolean);
|
||||
const nonce = params.get("nonce");
|
||||
const reauthenticated = params.get(REAUTH_MARKER) === "1";
|
||||
|
||||
useEffect(() => {
|
||||
if (started.current) return;
|
||||
started.current = true;
|
||||
|
||||
const reply = (message: AccessGrantedMessage) => {
|
||||
// Targeted at this origin rather than "*", so the grant result is not
|
||||
// broadcast to whatever else may be listening.
|
||||
window.opener?.postMessage(message, window.location.origin);
|
||||
};
|
||||
|
||||
const refuse = (message: string) => {
|
||||
setState("failed");
|
||||
setError(message);
|
||||
// The opener is told, so the tool settles now rather than waiting out
|
||||
// its timeout. Cross-origin openers never receive this: `reply` targets
|
||||
// this origin, so a window opened from somewhere else hears nothing.
|
||||
if (peerId) {
|
||||
reply({
|
||||
type: ACCESS_GRANTED_MESSAGE,
|
||||
ok: false,
|
||||
peerId,
|
||||
error: message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
if (!peerId || !wgPublicKey || !assistantPeerName || rules.length === 0) {
|
||||
setState("failed");
|
||||
setError("This link is missing what it needs to authorize anything.");
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
Checked before the sign-in, not after: a link that gets as far as an
|
||||
identity provider has already borrowed the credibility of one, and the
|
||||
person is answering a prompt they have no reason to distrust.
|
||||
*/
|
||||
if (!hasAccessNonce(nonce)) {
|
||||
refuse(
|
||||
"This window was not opened by the assistant, so nothing was authorized. " +
|
||||
"Start the request from the assistant panel in the dashboard.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rules.every(isAllowedAccessRule)) {
|
||||
refuse(
|
||||
"This link asks for access this flow does not grant, so nothing was authorized.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
Ask the identity provider to authenticate the person again, and come
|
||||
back here afterwards. The marker is what stops this repeating: without
|
||||
it the return trip would be indistinguishable from the first visit and
|
||||
the window would bounce to the IdP forever.
|
||||
*/
|
||||
if (!reauthenticated) {
|
||||
const back = `${window.location.pathname}${window.location.search}&${REAUTH_MARKER}=1`;
|
||||
await login(back, { prompt: "login" });
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
Spent on the way past, so a reload or a Back inside this window cannot
|
||||
grant a second time. The opener keeps its own copy, so this settles
|
||||
nothing there.
|
||||
*/
|
||||
if (!consumeAccessNonce(nonce)) {
|
||||
refuse(
|
||||
"This window has already been used, so nothing was authorized. " +
|
||||
"Start the request from the assistant panel in the dashboard.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await peerRequest.post(
|
||||
{ name: assistantPeerName, wg_pub_key: wgPublicKey, rules },
|
||||
`/${encodeURIComponent(peerId)}/temporary-access`,
|
||||
);
|
||||
setState("granted");
|
||||
reply({ type: ACCESS_GRANTED_MESSAGE, ok: true, peerId });
|
||||
} catch (err) {
|
||||
const detail =
|
||||
err instanceof Error ? err.message : "The request was refused.";
|
||||
setState("failed");
|
||||
setError(detail);
|
||||
reply({
|
||||
type: ACCESS_GRANTED_MESSAGE,
|
||||
ok: false,
|
||||
peerId,
|
||||
error: detail,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
run().catch(() => {
|
||||
setState("failed");
|
||||
setError("Something went wrong while authorizing.");
|
||||
});
|
||||
// Once, on mount. The parameters come from the URL and do not change.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Granting is the end of this window's job, so it sees itself out. The
|
||||
// opener has already been told; nothing here is waiting on a click.
|
||||
useEffect(() => {
|
||||
if (state !== "granted") return;
|
||||
const closing = window.setTimeout(() => window.close(), AUTO_CLOSE_MS);
|
||||
const refused = window.setTimeout(
|
||||
() => setCloseRefused(true),
|
||||
AUTO_CLOSE_MS + CLOSE_REFUSED_MS,
|
||||
);
|
||||
return () => {
|
||||
window.clearTimeout(closing);
|
||||
window.clearTimeout(refused);
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-nb-gray-950 px-8">
|
||||
<div className="flex max-w-md flex-col items-center gap-4 text-center">
|
||||
{state === "working" && (
|
||||
<>
|
||||
<Loader2Icon className="animate-spin text-netbird" size={32} />
|
||||
<h1 className="text-lg font-medium text-nb-gray-100">
|
||||
{reauthenticated ? "Granting access" : "Confirming it's you"}
|
||||
</h1>
|
||||
<p className="text-sm text-nb-gray-300">
|
||||
{reauthenticated
|
||||
? `Giving the assistant temporary SSH access to ${peerLabel}.`
|
||||
: "Signing you in again before anything is granted."}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{state === "granted" && (
|
||||
<>
|
||||
<CheckCircle2Icon className="text-green-500" size={32} />
|
||||
<h1 className="text-lg font-medium text-nb-gray-100">
|
||||
Successfully Authorized
|
||||
</h1>
|
||||
{/* Names the machine: this is the one moment the person is shown
|
||||
what they just granted access to. */}
|
||||
<p className="text-sm text-nb-gray-300">
|
||||
The access covers {peerLabel} only, and ends when the assistant
|
||||
disconnects.
|
||||
</p>
|
||||
{/* Only once the window has tried and failed to close itself —
|
||||
otherwise this is a button that disappears as it is read. */}
|
||||
{closeRefused && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.close()}
|
||||
className="mt-2 rounded-md bg-netbird px-4 py-2 text-sm font-medium text-white"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{state === "failed" && (
|
||||
<>
|
||||
<CircleXIcon className="text-red-500" size={32} />
|
||||
<h1 className="text-lg font-medium text-nb-gray-100">
|
||||
Authorization Failed
|
||||
</h1>
|
||||
<p className="text-sm text-nb-gray-300">
|
||||
{error} You can close this window; the assistant has been told it
|
||||
was refused.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+64
-160
@@ -6,34 +6,68 @@
|
||||
--toasts-before: 0;
|
||||
--lift: 1;
|
||||
|
||||
/*
|
||||
* The box full-screen overlays (modals) may occupy. Zero by default — it only
|
||||
* differs from the viewport while the assistant panel is revealed beside the
|
||||
* dashboard card, which writes these from AssistantPanelProvider so an overlay
|
||||
* stays inside the card instead of covering the panel.
|
||||
*/
|
||||
--nb-overlay-top: 0px;
|
||||
--nb-overlay-right: 0px;
|
||||
--nb-overlay-bottom: 0px;
|
||||
--nb-overlay-left: 0px;
|
||||
/* Matches the dashboard card's rounding while it is inset; 0 when it isn't. */
|
||||
--nb-overlay-radius: 0px;
|
||||
/* nb-gray INVERTED ramp (light mode) — space-separated RGB channels.
|
||||
High stops are surfaces (near-white), low stops are text (near-black).
|
||||
DEFAULT (the app background) sits slightly below white so white
|
||||
inputs, buttons and cards stand in front of it.
|
||||
The ramp is pure neutral (R = G = B) and matches the desktop client's
|
||||
light ramp (netbirdio/netbird#7344) so both UIs share one palette. */
|
||||
--nb-gray-DEFAULT: 243 243 243;
|
||||
--nb-gray-50: 26 26 26;
|
||||
--nb-gray-100: 33 33 33;
|
||||
--nb-gray-200: 50 50 50;
|
||||
--nb-gray-250: 58 58 58;
|
||||
--nb-gray-300: 77 77 77;
|
||||
--nb-gray-350: 92 92 92;
|
||||
--nb-gray-400: 108 108 108;
|
||||
--nb-gray-500: 135 135 135;
|
||||
--nb-gray-600: 154 154 154;
|
||||
--nb-gray-700: 209 209 209;
|
||||
--nb-gray-800: 226 226 226;
|
||||
--nb-gray-850: 234 234 234;
|
||||
--nb-gray-900: 238 238 238;
|
||||
--nb-gray-910: 240 240 240;
|
||||
--nb-gray-920: 243 243 243;
|
||||
--nb-gray-925: 245 245 245;
|
||||
--nb-gray-930: 246 246 246;
|
||||
--nb-gray-935: 248 248 248;
|
||||
--nb-gray-940: 250 250 250;
|
||||
--nb-gray-950: 252 252 252;
|
||||
--nb-gray-960: 255 255 255;
|
||||
|
||||
/* Loading-skeleton colours, consumed by ThemeProvider's SkeletonTheme. */
|
||||
--skeleton-base: 238 238 238;
|
||||
--skeleton-highlight: 247 247 247;
|
||||
}
|
||||
|
||||
/*
|
||||
* Drop-in replacement for `fixed inset-0` on overlays that must respect the
|
||||
* assistant panel. Transitioned to match the card's own geometry transition, so
|
||||
* an overlay that is already open follows the card as the panel opens/closes.
|
||||
*/
|
||||
.nb-overlay-inset {
|
||||
position: fixed;
|
||||
top: var(--nb-overlay-top);
|
||||
right: var(--nb-overlay-right);
|
||||
bottom: var(--nb-overlay-bottom);
|
||||
left: var(--nb-overlay-left);
|
||||
border-radius: var(--nb-overlay-radius);
|
||||
transition: top 300ms ease-out, right 300ms ease-out,
|
||||
bottom 300ms ease-out, left 300ms ease-out;
|
||||
.dark {
|
||||
/* nb-gray ORIGINAL ramp (dark mode) */
|
||||
--nb-gray-DEFAULT: 24 26 29;
|
||||
--nb-gray-50: 244 246 247;
|
||||
--nb-gray-100: 228 231 233;
|
||||
--nb-gray-200: 203 210 214;
|
||||
--nb-gray-250: 183 192 198;
|
||||
--nb-gray-300: 163 173 181;
|
||||
--nb-gray-350: 143 156 168;
|
||||
--nb-gray-400: 124 137 148;
|
||||
--nb-gray-500: 97 110 121;
|
||||
--nb-gray-600: 83 93 103;
|
||||
--nb-gray-700: 71 78 87;
|
||||
--nb-gray-800: 63 68 75;
|
||||
--nb-gray-850: 54 59 64;
|
||||
--nb-gray-900: 46 50 56;
|
||||
--nb-gray-910: 43 47 51;
|
||||
--nb-gray-920: 37 40 45;
|
||||
--nb-gray-925: 30 33 35;
|
||||
--nb-gray-930: 37 40 44;
|
||||
--nb-gray-935: 31 33 36;
|
||||
--nb-gray-940: 28 30 33;
|
||||
--nb-gray-950: 24 26 29;
|
||||
--nb-gray-960: 22 24 27;
|
||||
|
||||
/* Dark skeletons keep their existing nb-gray-920 / nb-gray-850 values. */
|
||||
--skeleton-base: 37 40 45;
|
||||
--skeleton-highlight: 54 59 64;
|
||||
}
|
||||
|
||||
html{
|
||||
@@ -41,13 +75,13 @@ html{
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-2xl font-medium text-gray-700 dark:text-nb-gray-100 my-1;
|
||||
@apply text-2xl font-medium text-nb-gray-100 my-1;
|
||||
}
|
||||
h2 {
|
||||
@apply text-xl font-medium text-gray-700 dark:text-nb-gray-100 my-1;
|
||||
@apply text-xl font-medium text-nb-gray-100 my-1;
|
||||
}
|
||||
p {
|
||||
@apply font-light tracking-wide text-gray-700 dark:text-zinc-50 text-sm;
|
||||
@apply font-light tracking-wide text-nb-gray-200 text-sm;
|
||||
}
|
||||
|
||||
.p-default {
|
||||
@@ -63,37 +97,6 @@ p {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
/*
|
||||
* Slim scrollbar for panes that scroll their own content, matching the Radix
|
||||
* `ScrollBar` thumb. Used where a native scroll container is required and the
|
||||
* Radix component can't wrap it — see the assistant thread's viewport.
|
||||
*/
|
||||
.nb-scrollbar {
|
||||
scrollbar-width: thin; /* Firefox */
|
||||
scrollbar-color: theme("colors.nb-gray.800") transparent;
|
||||
}
|
||||
|
||||
.nb-scrollbar::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.nb-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.nb-scrollbar::-webkit-scrollbar-thumb {
|
||||
/* Inset via a transparent border so the thumb reads as 8px inside a 10px gutter. */
|
||||
background-color: theme("colors.nb-gray.800");
|
||||
border: 1px solid transparent;
|
||||
background-clip: content-box;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.nb-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background-color: theme("colors.nb-gray.700");
|
||||
}
|
||||
|
||||
[placeholder]{
|
||||
text-overflow:ellipsis;
|
||||
}
|
||||
@@ -155,7 +158,7 @@ p {
|
||||
}
|
||||
|
||||
.timescape input:focus {
|
||||
@apply bg-nb-gray-700 text-white rounded py-1 px-0.5 border-0 outline-0;
|
||||
@apply bg-nb-gray-700 text-nb-gray-50 rounded py-1 px-0.5 border-0 outline-0;
|
||||
}
|
||||
|
||||
.timescape .separator {
|
||||
@@ -243,82 +246,6 @@ p {
|
||||
@apply border-netbird;
|
||||
}
|
||||
|
||||
/*
|
||||
* A fenced block with a header is one box, not a bar on top of a box: the
|
||||
* `pre` drops its top edge and the gap between them. Done here rather than in
|
||||
* the `pre` renderer because that component can't see whether a header was
|
||||
* rendered — `CodeHeader` skips one-liners.
|
||||
*/
|
||||
.assistant-markdown .assistant-code-header + pre {
|
||||
@apply mt-0 rounded-t-none border-t-0 pt-1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assistant code block tokens (lowlight → hljs classes).
|
||||
*
|
||||
* A hand-written palette rather than one of highlight.js's stylesheets: those
|
||||
* set their own background and a foreground that fights the panel's. Scoped to
|
||||
* `.assistant-markdown` so it can't reach the dashboard's other code surfaces,
|
||||
* and limited to the token classes the registered grammars actually emit.
|
||||
*
|
||||
* Two hues and the greys, deliberately: a full editor palette turns a
|
||||
* three-line install command into the loudest thing in the conversation.
|
||||
*/
|
||||
.assistant-markdown .hljs-comment,
|
||||
.assistant-markdown .hljs-quote {
|
||||
@apply text-nb-gray-500 italic;
|
||||
}
|
||||
|
||||
.assistant-markdown .hljs-keyword,
|
||||
.assistant-markdown .hljs-selector-tag,
|
||||
.assistant-markdown .hljs-literal,
|
||||
.assistant-markdown .hljs-section,
|
||||
.assistant-markdown .hljs-doctag,
|
||||
.assistant-markdown .hljs-type {
|
||||
@apply text-netbird-300;
|
||||
}
|
||||
|
||||
.assistant-markdown .hljs-string,
|
||||
.assistant-markdown .hljs-regexp,
|
||||
.assistant-markdown .hljs-addition {
|
||||
@apply text-green-400;
|
||||
}
|
||||
|
||||
.assistant-markdown .hljs-number,
|
||||
.assistant-markdown .hljs-symbol,
|
||||
.assistant-markdown .hljs-bullet,
|
||||
.assistant-markdown .hljs-meta {
|
||||
@apply text-nb-gray-300;
|
||||
}
|
||||
|
||||
.assistant-markdown .hljs-attr,
|
||||
.assistant-markdown .hljs-attribute,
|
||||
.assistant-markdown .hljs-variable,
|
||||
.assistant-markdown .hljs-template-variable,
|
||||
.assistant-markdown .hljs-selector-attr {
|
||||
@apply text-nb-gray-200;
|
||||
}
|
||||
|
||||
.assistant-markdown .hljs-title,
|
||||
.assistant-markdown .hljs-name,
|
||||
.assistant-markdown .hljs-built_in,
|
||||
.assistant-markdown .hljs-selector-id,
|
||||
.assistant-markdown .hljs-selector-class {
|
||||
@apply text-nb-gray-100 font-medium;
|
||||
}
|
||||
|
||||
.assistant-markdown .hljs-deletion {
|
||||
@apply text-red-400;
|
||||
}
|
||||
|
||||
.assistant-markdown .hljs-emphasis {
|
||||
@apply italic;
|
||||
}
|
||||
|
||||
.assistant-markdown .hljs-strong {
|
||||
@apply font-semibold;
|
||||
}
|
||||
|
||||
/* Keep drag-to-connect line behind nodes (default is above) */
|
||||
.react-flow__connectionline,
|
||||
.react-flow__connection {
|
||||
@@ -366,29 +293,6 @@ p {
|
||||
animation: cc-node-pulse 1.1s ease-out 2;
|
||||
}
|
||||
|
||||
/* A node arriving on the canvas (the assistant placing one, a drop). The
|
||||
animation goes on the node's CARD, not the wrapper: React Flow owns the
|
||||
wrapper's transform for positioning, so animating that would fight it. */
|
||||
@keyframes cc-node-spawn {
|
||||
from { opacity: 0; transform: scale(0.94); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
.react-flow__node.cc-node-spawn > * {
|
||||
animation: cc-node-spawn 280ms cubic-bezier(0.2, 0.8, 0.3, 1) both;
|
||||
}
|
||||
|
||||
/* A connection announcing itself. Deliberately a GLOW, not a recolour: edge
|
||||
colour carries meaning here (one blue line = one-way, two green lines =
|
||||
bidirectional), so tinting a path would have said something false — and
|
||||
ending a keyframe on `stroke: inherit` made the line vanish outright. */
|
||||
@keyframes cc-edge-flash {
|
||||
0% { filter: drop-shadow(0 0 7px rgba(56, 189, 248, 0.95)); stroke-width: 4; }
|
||||
100% { filter: drop-shadow(0 0 0 rgba(56, 189, 248, 0)); stroke-width: 2; }
|
||||
}
|
||||
.react-flow__edge.cc-edge-flash .react-flow__edge-path {
|
||||
animation: cc-edge-flash 1.2s ease-out both;
|
||||
}
|
||||
|
||||
/* Nodes/edges off the focused node's path fade out while its panel is open. */
|
||||
.react-flow__node.cc-dimmed {
|
||||
opacity: 0.25;
|
||||
|
||||
@@ -111,7 +111,7 @@ function InviteAcceptContent() {
|
||||
<Clock className="w-8 h-8 text-yellow-500" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-2xl font-semibold text-white mb-2">
|
||||
<h1 className="text-2xl font-semibold dark:text-white text-nb-gray-900 mb-2">
|
||||
Too Many Requests
|
||||
</h1>
|
||||
<Paragraph className="text-nb-gray-400 text-base">
|
||||
@@ -138,7 +138,7 @@ function InviteAcceptContent() {
|
||||
<AlertCircle className="w-8 h-8 text-red-500" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-2xl font-semibold text-white mb-2">
|
||||
<h1 className="text-2xl font-semibold dark:text-white text-nb-gray-900 mb-2">
|
||||
Invalid Invite
|
||||
</h1>
|
||||
<Paragraph className="text-nb-gray-400 text-base">
|
||||
@@ -166,7 +166,7 @@ function InviteAcceptContent() {
|
||||
<CheckCircle2 className="w-8 h-8 text-green-500" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-2xl font-semibold text-white mb-2">
|
||||
<h1 className="text-2xl font-semibold dark:text-white text-nb-gray-900 mb-2">
|
||||
Account Created!
|
||||
</h1>
|
||||
<Paragraph className="text-nb-gray-400">
|
||||
@@ -194,7 +194,7 @@ function InviteAcceptContent() {
|
||||
<AlertCircle className="w-8 h-8 text-yellow-500" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-2xl font-semibold text-white mb-2">
|
||||
<h1 className="text-2xl font-semibold dark:text-white text-nb-gray-900 mb-2">
|
||||
Invite Expired
|
||||
</h1>
|
||||
<Paragraph className="text-nb-gray-400">
|
||||
@@ -221,7 +221,7 @@ function InviteAcceptContent() {
|
||||
</div>
|
||||
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-2xl font-semibold text-white mb-2">
|
||||
<h1 className="text-2xl font-semibold dark:text-white text-nb-gray-900 mb-2">
|
||||
Welcome to NetBird
|
||||
</h1>
|
||||
<p className="dark:text-nb-gray-400 text-nb-gray-500 text-base">
|
||||
@@ -235,7 +235,7 @@ function InviteAcceptContent() {
|
||||
<User2 className="w-5 h-5 text-nb-gray-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-white font-medium">{inviteInfo.name}</div>
|
||||
<div className="dark:text-white text-nb-gray-900 font-medium">{inviteInfo.name}</div>
|
||||
<div className="text-nb-gray-400 text-sm flex items-center gap-1">
|
||||
<Mail className="w-3 h-3" />
|
||||
{inviteInfo.email}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* Lucide's `message-square-dot` with the corner dot swapped for the `sparkle`
|
||||
* glyph. The sparkle is scaled down, so its stroke width is scaled up by the
|
||||
* inverse to keep the rendered weight matching the message outline.
|
||||
*/
|
||||
export function AssistantIcon({
|
||||
size = 15,
|
||||
className,
|
||||
}: {
|
||||
size?: number;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12.7 3H4a2 2 0 0 0-2 2v16.286a.71.71 0 0 0 1.212.502l2.202-2.202A2 2 0 0 1 6.828 19H20a2 2 0 0 0 2-2v-4.7" />
|
||||
<g transform="translate(12.72 0.72) scale(0.44)" strokeWidth={4.55}>
|
||||
<path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
Loaded 20 of 232 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user