Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/shared/api/contracts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './veil-node.contract'
36 changes: 36 additions & 0 deletions src/shared/api/contracts/veil-node.contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Inline contract extension that adds the `core: 'XRAY' | 'VEIL'`
// field to the Create/Update node request schemas.
//
// Why this lives here:
// @remnawave/backend-contract@2.7.2 (the version pinned by this
// branch) ships the legacy XRAY-only schemas. The matching backend
// PR (remnawave/backend#168) adds the column and accepts the field
// on the wire, but until a new backend-contract release is published
// to npm the frontend has nothing to import.
//
// MIGRATION: once @remnawave/backend-contract publishes a release that
// includes `NodeCore`, replace every import of
// `CreateVeilAwareNodeCommand` / `UpdateVeilAwareNodeCommand` with
// `CreateNodeCommand` / `UpdateNodeCommand` from the package and
// delete this file.

import { CreateNodeCommand, UpdateNodeCommand } from '@remnawave/backend-contract'
import { z } from 'zod'

import { DEFAULT_NODE_CORE, NODE_CORE } from '@shared/constants/veil'

const NodeCoreSchema = z
.enum([NODE_CORE.XRAY, NODE_CORE.VEIL] as const)
.default(DEFAULT_NODE_CORE)

export const CreateVeilAwareNodeRequestSchema = CreateNodeCommand.RequestSchema.extend({
core: NodeCoreSchema,
})

export type CreateVeilAwareNodeRequest = z.infer<typeof CreateVeilAwareNodeRequestSchema>;

export const UpdateVeilAwareNodeRequestSchema = UpdateNodeCommand.RequestSchema.extend({
core: NodeCoreSchema.optional(),
})

export type UpdateVeilAwareNodeRequest = z.infer<typeof UpdateVeilAwareNodeRequestSchema>;
14 changes: 12 additions & 2 deletions src/shared/api/hooks/nodes/nodes.mutation.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,21 @@ import {
} from '@remnawave/backend-contract'
import { notifications } from '@mantine/notifications'

import {
CreateVeilAwareNodeRequestSchema,
UpdateVeilAwareNodeRequestSchema
} from '@shared/api/contracts'

import { createMutationHook } from '../../tsq-helpers'

// Body schemas point at Veil-aware extensions of the upstream
// CreateNodeCommand / UpdateNodeCommand schemas. They are pure
// supersets — every existing XRAY-only payload still parses unchanged.
// See `@shared/api/contracts/veil-node.contract` for the swap-out plan
// once @remnawave/backend-contract publishes the upstream `core` field.
export const useCreateNode = createMutationHook({
endpoint: CreateNodeCommand.TSQ_url,
bodySchema: CreateNodeCommand.RequestSchema,
bodySchema: CreateVeilAwareNodeRequestSchema,
responseSchema: CreateNodeCommand.ResponseSchema,
requestMethod: CreateNodeCommand.endpointDetails.REQUEST_METHOD,
rMutationParams: {
Expand All @@ -42,7 +52,7 @@ export const useCreateNode = createMutationHook({

export const useUpdateNode = createMutationHook({
endpoint: UpdateNodeCommand.TSQ_url,
bodySchema: UpdateNodeCommand.RequestSchema,
bodySchema: UpdateVeilAwareNodeRequestSchema,
responseSchema: UpdateNodeCommand.ResponseSchema,
requestMethod: UpdateNodeCommand.endpointDetails.REQUEST_METHOD,
rMutationParams: {
Expand Down
1 change: 1 addition & 0 deletions src/shared/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './interfaces'
export * from './monaco-theme'
export * from './routes'
export * from './theme'
export * from './veil'
1 change: 1 addition & 0 deletions src/shared/constants/veil/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './node-core'
26 changes: 26 additions & 0 deletions src/shared/constants/veil/node-core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Node core flavours. Mirrors the `Nodes.core` enum that the panel-side
// PR adds (remnawave/backend#168) and the controller surface that
// remnawave/node#38 exposes under `/node/veil/*`.
//
// Lives client-side because @remnawave/backend-contract@2.7.2 (the
// version this branch pins) doesn't yet ship `NodeCore`; once the
// backend release that does is published, swap the imports here for
// the upstream schema and delete this file.

export const NODE_CORE = {
XRAY: 'XRAY',
VEIL: 'VEIL',
} as const

export type TNodeCore = (typeof NODE_CORE)[keyof typeof NODE_CORE];

export const NODE_CORE_VALUES: TNodeCore[] = [NODE_CORE.XRAY, NODE_CORE.VEIL]

export const DEFAULT_NODE_CORE: TNodeCore = NODE_CORE.XRAY

// Human-friendly labels surfaced in the Mantine `Select`. Kept here so
// the create + edit forms stay in lock-step.
export const NODE_CORE_LABELS: Record<TNodeCore, string> = {
XRAY: 'Xray-core',
VEIL: 'Veil (pre-alpha)',
}
29 changes: 28 additions & 1 deletion src/shared/ui/forms/nodes/base-node-form/node-vitals.card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import {
GetPubKeyCommand,
UpdateNodeCommand
} from '@remnawave/backend-contract'
import { TbCertificate, TbMapPin, TbPackage, TbUserCheck, TbWorld } from 'react-icons/tb'
import {
TbCertificate,
TbCpu,
TbMapPin,
TbPackage,
TbUserCheck,
TbWorld
} from 'react-icons/tb'
import { ForwardRefComponent, HTMLMotionProps, Variants } from 'motion/react'
import { Group, NumberInput, Select, Stack, TextInput } from '@mantine/core'
import { UseFormReturnType } from '@mantine/form'
Expand All @@ -13,10 +20,16 @@ import { useTranslation } from 'react-i18next'

import { CopyableFieldShared } from '@shared/ui/copyable-field/copyable-field'
import { BaseOverlayHeader } from '@shared/ui/overlays/base-overlay-header'
import { NODE_CORE, NODE_CORE_LABELS } from '@shared/constants/veil'
import { SectionCard } from '@shared/ui/section-card'

import { COUNTRIES } from './constants'

const NODE_CORE_OPTIONS = [
{ value: NODE_CORE.XRAY, label: NODE_CORE_LABELS.XRAY },
{ value: NODE_CORE.VEIL, label: NODE_CORE_LABELS.VEIL }
]

interface IProps<T extends CreateNodeCommand.Request | UpdateNodeCommand.Request> {
cardVariants: Variants
form: UseFormReturnType<T>
Expand Down Expand Up @@ -61,6 +74,20 @@ export const NodeVitalsCard = <T extends CreateNodeCommand.Request | UpdateNodeC
}}
/>

<Select
allowDeselect={false}
data={NODE_CORE_OPTIONS}
description="Veil ships an opaque server.yaml from the panel; pre-alpha"
key={form.key('core' as never)}
label="Proxy core"
leftSection={<TbCpu size={16} />}
required
styles={{
label: { fontWeight: 500 }
}}
{...form.getInputProps('core' as never)}
/>

<TextInput
key={form.key('name')}
label={t('base-node-form.internal-name')}
Expand Down
1 change: 1 addition & 0 deletions src/shared/ui/logos/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export * from './mihomo-logo'
export * from './pockeid-logo'
export * from './singbox-logo'
export * from './stash-logo'
export * from './veil-logo'
export * from './xray-logo'
export * from './yandex-logo'
47 changes: 47 additions & 0 deletions src/shared/ui/logos/veil-logo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* eslint-disable @stylistic/indent */
import { Box, BoxProps, ElementProps } from '@mantine/core'

interface LogoProps
extends ElementProps<'svg', keyof BoxProps>,
Omit<BoxProps, 'children' | 'ref'> {
size?: number | string
}

// Stylized "V" inside a shield — visually distinct from XrayLogo so
// operators can tell at a glance which core a node is running.
export function VeilLogo({ size = 20, style, ...props }: LogoProps) {
return (
<Box
component="svg"
fill="none"
preserveAspectRatio="xMidYMid meet"
style={{
width: size,
height: size,
display: 'inline-block',
verticalAlign: 'middle',
flexShrink: 0,
...style
}}
viewBox="0 0 35 35"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M17.5 2L4 7.2v9.1c0 8.1 5.7 15.7 13.5 17.7C25.3 32 31 24.4 31 16.3V7.2L17.5 2z"
fill="currentColor"
fillOpacity="0.18"
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="1.6"
/>
<path
d="M11.5 12.2 17.5 24l6-11.8"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2.4"
/>
</Box>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ import { useEffect, useState } from 'react'
import { useForm } from '@mantine/form'
import { TbCpu } from 'react-icons/tb'

import {
CreateVeilAwareNodeRequest,
CreateVeilAwareNodeRequestSchema
} from '@shared/api/contracts'
import { useNodesStoreActions, useNodesStoreCreateModalIsOpen } from '@entities/dashboard/nodes'
import { configProfilesQueryKeys, useCreateNode, useGetPubKey } from '@shared/api/hooks'
import { BaseOverlayHeader } from '@shared/ui/overlays/base-overlay-header'
import { DEFAULT_NODE_CORE } from '@shared/constants/veil'
import { gbToBytesUtil } from '@shared/utils/bytes'
import { queryClient } from '@shared/api'

Expand All @@ -31,10 +36,10 @@ export const CreateNodeModalWidget = () => {
const [createdNodeUuid, setCreatedNodeUuid] = useState<string>()
const [selectedPort, setSelectedPort] = useState<number>(2222)

const form = useForm<CreateNodeCommand.Request>({
const form = useForm<CreateVeilAwareNodeRequest>({
name: 'create-node-form',
mode: 'uncontrolled',
validate: zodResolver(CreateNodeCommand.RequestSchema)
validate: zodResolver(CreateVeilAwareNodeRequestSchema)
})

const handleClose = () => {
Expand Down Expand Up @@ -70,7 +75,7 @@ export const CreateNodeModalWidget = () => {
name: values.name.trim(),
address: values.address.trim(),
trafficLimitBytes: gbToBytesUtil(values.trafficLimitBytes)
}
} satisfies CreateNodeCommand.Request & { core: CreateVeilAwareNodeRequest['core'] }
})
}

Expand All @@ -83,7 +88,8 @@ export const CreateNodeModalWidget = () => {
}

form.setValues({
port: 2222
port: 2222,
core: DEFAULT_NODE_CORE
})
}, [form])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,26 @@ import {
Text,
TextInput
} from '@mantine/core'
import { TbCertificate, TbId, TbMapPin, TbWorld } from 'react-icons/tb'
import { CreateNodeCommand } from '@remnawave/backend-contract'
import { TbCertificate, TbCpu, TbId, TbMapPin, TbWorld } from 'react-icons/tb'
import { UseFormReturnType } from '@mantine/form'
import { useTranslation } from 'react-i18next'
import { PiArrowRight } from 'react-icons/pi'

import { CopyableFieldShared } from '@shared/ui/copyable-field/copyable-field'
import { COUNTRIES } from '@shared/ui/forms/nodes/base-node-form/constants'
import { NODE_CORE, NODE_CORE_LABELS } from '@shared/constants/veil'
import { CreateVeilAwareNodeRequest } from '@shared/api/contracts'

import { CopyDockerComposeWidget } from './copy-docker-compose.widget'

const NODE_CORE_OPTIONS = [
{ value: NODE_CORE.XRAY, label: NODE_CORE_LABELS.XRAY },
{ value: NODE_CORE.VEIL, label: NODE_CORE_LABELS.VEIL }
]

interface IProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
form: UseFormReturnType<CreateNodeCommand.Request, any>
form: UseFormReturnType<CreateVeilAwareNodeRequest, any>
onNext: () => void
port: number
pubKey: string | undefined
Expand All @@ -37,12 +43,14 @@ export const CreateNodeStep1Connection = ({ form, onNext, pubKey, port }: IProps
const countryCodeErrors = form.validateField('countryCode')
const addressErrors = form.validateField('address')
const portErrors = form.validateField('port')
const coreErrors = form.validateField('core')

if (
nameErrors.hasError ||
countryCodeErrors.hasError ||
addressErrors.hasError ||
portErrors.hasError
portErrors.hasError ||
coreErrors.hasError
) {
return
}
Expand Down Expand Up @@ -112,6 +120,21 @@ export const CreateNodeStep1Connection = ({ form, onNext, pubKey, port }: IProps
}}
/>

<Select
allowDeselect={false}
data={NODE_CORE_OPTIONS}
description="Veil ships an opaque server.yaml from the panel; pre-alpha"
key={form.key('core')}
label="Proxy core"
leftSection={<TbCpu size={16} />}
required
size="sm"
styles={{
label: { fontWeight: 500 }
}}
{...form.getInputProps('core')}
/>

<Group align="flex-start" gap="xs" w="100%">
<TextInput
key={form.key('address')}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { CreateNodeCommand } from '@remnawave/backend-contract'
import { Button, Group, Skeleton, Stack } from '@mantine/core'
import { UseFormReturnType } from '@mantine/form'
import { useTranslation } from 'react-i18next'
import { PiArrowLeft } from 'react-icons/pi'
import { TbCheck } from 'react-icons/tb'

import { ShowConfigProfilesWithInboundsFeature } from '@features/ui/dashboard/nodes/show-config-profiles-with-inbounds'
import { CreateVeilAwareNodeRequest } from '@shared/api/contracts'
import { useGetConfigProfiles } from '@shared/api/hooks'

import { CopyDockerComposeWidget } from './copy-docker-compose.widget'

interface IProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
form: UseFormReturnType<CreateNodeCommand.Request, any>
form: UseFormReturnType<CreateVeilAwareNodeRequest, any>
isCreating: boolean
onCreateNode: () => void
onPrev: () => void
Expand Down
Loading