refactor: build info modal

This commit is contained in:
kastov 2025-12-30 05:19:09 +03:00
parent 0db4f67329
commit f934f2dce9
No known key found for this signature in database
GPG key ID: 1B27BE29057F4C90
19 changed files with 407 additions and 312 deletions

View file

@ -35,29 +35,6 @@ jobs:
with: with:
node-version: '22.18.0' node-version: '22.18.0'
- name: Generate build-info.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-8)
BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
BRANCH="${{ github.ref_name }}"
FULL_SHA="${{ github.sha }}"
TAG=$(grep -m1 '"version":' package.json | cut -d'"' -f4)
COMMIT_URL="https://github.com/${{ github.repository }}/commit/$SHORT_SHA"
cat <<EOF > build.info.json
{
"buildTime": "$BUILD_TIME",
"commitFull": "$FULL_SHA",
"commit": "$SHORT_SHA",
"tag": $( [ "$TAG" = "null" ] && echo null || echo "\"$TAG\"" ),
"branch": "$BRANCH",
"commitUrl": "$COMMIT_URL"
}
EOF
- name: Install dependencies - name: Install dependencies
run: | run: |
npm ci npm ci

View file

@ -40,33 +40,6 @@ jobs:
with: with:
node-version: '22.x' node-version: '22.x'
- name: Generate build-info.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-8)
BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
BRANCH="main"
FULL_SHA="${{ github.sha }}"
TAG="null"
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
TAG="${{ github.ref_name }}"
fi
COMMIT_URL="https://github.com/${{ github.repository }}/commit/$SHORT_SHA"
cat <<EOF > build.info.json
{
"buildTime": "$BUILD_TIME",
"commitFull": "$FULL_SHA",
"commit": "$SHORT_SHA",
"tag": $( [ "$TAG" = "null" ] && echo null || echo "\"$TAG\"" ),
"branch": "$BRANCH",
"commitUrl": "$COMMIT_URL"
}
EOF
- name: Generate changelog - name: Generate changelog
id: changelog id: changelog
run: | run: |

2
.gitignore vendored
View file

@ -137,8 +137,6 @@ fsd-high-level-dependencies.html
wip/** wip/**
wip/ wip/
build.info.json
public/wasm_exec.js public/wasm_exec.js
public/xray.schema.json public/xray.schema.json
public/main.wasm public/main.wasm

8
package-lock.json generated
View file

@ -35,7 +35,7 @@
"@monaco-editor/react": "^4.7.0", "@monaco-editor/react": "^4.7.0",
"@noble/post-quantum": "^0.5.2", "@noble/post-quantum": "^0.5.2",
"@paralleldrive/cuid2": "2.2.2", "@paralleldrive/cuid2": "2.2.2",
"@remnawave/backend-contract": "2.5.4", "@remnawave/backend-contract": "2.5.5",
"@remnawave/subscription-page-types": "0.3.3", "@remnawave/subscription-page-types": "0.3.3",
"@simplewebauthn/browser": "^13.2.2", "@simplewebauthn/browser": "^13.2.2",
"@stablelib/base64": "^2.0.1", "@stablelib/base64": "^2.0.1",
@ -2839,9 +2839,9 @@
} }
}, },
"node_modules/@remnawave/backend-contract": { "node_modules/@remnawave/backend-contract": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@remnawave/backend-contract/-/backend-contract-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@remnawave/backend-contract/-/backend-contract-2.5.5.tgz",
"integrity": "sha512-ON0Ui/9o/ef+SxYBdil8S9yQseMBvfy9UKpXJ+QfU+IqZ5cOVbGOEwtuqR1FHs5Z2kHTZ0qY9RXEoPxp7cJyGw==", "integrity": "sha512-6CL7WOsY+T7YuPH7w2aRirhhxTvbRUQFOZUxVulGbspOmzcMzSPiG1NRLdkJ5FVBnx4EpprQsydoE45OLuAJ2A==",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"zod": "3.25.76" "zod": "3.25.76"

View file

@ -59,7 +59,7 @@
"@monaco-editor/react": "^4.7.0", "@monaco-editor/react": "^4.7.0",
"@noble/post-quantum": "^0.5.2", "@noble/post-quantum": "^0.5.2",
"@paralleldrive/cuid2": "2.2.2", "@paralleldrive/cuid2": "2.2.2",
"@remnawave/backend-contract": "2.5.4", "@remnawave/backend-contract": "2.5.5",
"@remnawave/subscription-page-types": "0.3.3", "@remnawave/subscription-page-types": "0.3.3",
"@simplewebauthn/browser": "^13.2.2", "@simplewebauthn/browser": "^13.2.2",
"@stablelib/base64": "^2.0.1", "@stablelib/base64": "^2.0.1",

View file

@ -10,6 +10,7 @@ import { SidebarTitleShared } from '@shared/ui/sidebar/sidebar-title'
import { SidebarLogoShared } from '@shared/ui/sidebar/sidebar-logo' import { SidebarLogoShared } from '@shared/ui/sidebar/sidebar-logo'
import { HeaderControls } from '@shared/ui/header-buttons' import { HeaderControls } from '@shared/ui/header-buttons'
import { HelpDrawerShared } from '@shared/ui/help-drawer' import { HelpDrawerShared } from '@shared/ui/help-drawer'
import { GameModalShared } from '@shared/ui/sidebar'
import { Navigation } from './navbar/navigation.layout' import { Navigation } from './navbar/navigation.layout'
import classes from './Main.module.css' import classes from './Main.module.css'
@ -118,6 +119,7 @@ export function MainLayout() {
</AppShell.Section> </AppShell.Section>
<AppShell.Section className={classes.footerSection}> <AppShell.Section className={classes.footerSection}>
<GameModalShared />
{isSocialButton && ( {isSocialButton && (
<Group justify="center" mt="md" style={{ flexShrink: 0 }}> <Group justify="center" mt="md" style={{ flexShrink: 0 }}>
<HeaderControls <HeaderControls

View file

@ -1,5 +1,6 @@
import { import {
GetBandwidthStatsCommand, GetBandwidthStatsCommand,
GetMetadataCommand,
GetNodesMetricsCommand, GetNodesMetricsCommand,
GetNodesStatisticsCommand, GetNodesStatisticsCommand,
GetRemnawaveHealthCommand, GetRemnawaveHealthCommand,
@ -30,6 +31,9 @@ export const systemQueryKeys = createQueryKeys('system', {
}, },
getNodesMetrics: { getNodesMetrics: {
queryKey: null queryKey: null
},
getRemnawaveMetadata: {
queryKey: null
} }
}) })
@ -104,3 +108,15 @@ export const useGetNodesMetrics = createGetQueryHook({
}, },
errorHandler: (error) => errorHandler(error, 'Get Nodes Metrics') errorHandler: (error) => errorHandler(error, 'Get Nodes Metrics')
}) })
export const useGetRemnawaveMetadata = createGetQueryHook({
endpoint: GetMetadataCommand.TSQ_url,
responseSchema: GetMetadataCommand.ResponseSchema,
getQueryKey: () => systemQueryKeys.getRemnawaveMetadata.queryKey,
rQueryParams: {
placeholderData: keepPreviousData,
refetchOnMount: false,
staleTime: sToMs(3_600)
},
errorHandler: (error) => errorHandler(error, 'Get Remnawave Metadata')
})

View file

@ -14,6 +14,13 @@
transition: border-color 150ms ease; transition: border-color 150ms ease;
} }
.container.small {
gap: 8px;
padding: 8px 12px;
font-size: var(--mantine-font-size-xs);
border-radius: var(--mantine-radius-sm);
}
.container:hover { .container:hover {
border-color: var(--mantine-color-dark-4); border-color: var(--mantine-color-dark-4);
} }
@ -36,6 +43,10 @@
z-index: 1; z-index: 1;
} }
.small .codeWrapper::after {
width: 24px;
}
.code { .code {
overflow-x: auto; overflow-x: auto;
white-space: nowrap; white-space: nowrap;

View file

@ -1,27 +1,37 @@
import { ActionIcon, Box, CopyButton } from '@mantine/core' import { ActionIcon, Box, CopyButton } from '@mantine/core'
import { PiCheck, PiCopy } from 'react-icons/pi' import { PiCheck, PiCopy } from 'react-icons/pi'
import clsx from 'clsx'
import styles from './copyable-code-block.module.css' import styles from './copyable-code-block.module.css'
interface IProps { interface IProps {
size?: 'normal' | 'small'
value: string value: string
} }
export function CopyableCodeBlock({ value }: IProps) { export function CopyableCodeBlock({ value, size = 'normal' }: IProps) {
const isSmall = size === 'small'
const iconSize = isSmall ? 14 : 18
return ( return (
<CopyButton timeout={2000} value={value}> <CopyButton timeout={2000} value={value}>
{({ copied, copy }) => ( {({ copied, copy }) => (
<Box className={styles.container} onClick={copy}> <Box
className={clsx(styles.container, {
[styles.small]: isSmall
})}
onClick={copy}
>
<Box className={styles.codeWrapper}> <Box className={styles.codeWrapper}>
<Box className={styles.code}>{value}</Box> <Box className={styles.code}>{value}</Box>
</Box> </Box>
<ActionIcon <ActionIcon
className={styles.copyButton} className={styles.copyButton}
data-copied={copied} data-copied={copied}
size="sm" size={isSmall ? 'xs' : 'sm'}
variant="transparent" variant="transparent"
> >
{copied ? <PiCheck size={18} /> : <PiCopy size={18} />} {copied ? <PiCheck size={iconSize} /> : <PiCopy size={iconSize} />}
</ActionIcon> </ActionIcon>
</Box> </Box>
)} )}

View file

@ -0,0 +1,34 @@
.skeleton {
position: relative;
overflow: hidden;
border: 1px solid;
border-color: var(--mantine-color-dark-4);
border-radius: var(--mantine-radius-lg);
background: linear-gradient(
135deg,
var(--mantine-color-dark-6) 0%,
var(--mantine-color-dark-7) 100%
);
}
.skeleton::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.06) 50%,
transparent 100%
);
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}

View file

@ -0,0 +1,11 @@
import { Box } from '@mantine/core'
import classes from './SkeletonHeaderControl.module.css'
interface SkeletonHeaderControlProps {
width?: number | string
}
export function SkeletonHeaderControl({ width = 44 }: SkeletonHeaderControlProps) {
return <Box className={classes.skeleton} h={44} w={width} />
}

View file

@ -15,11 +15,18 @@
.version { .version {
cursor: pointer; cursor: pointer;
min-width: 85px;
&.newVersion { &.newVersion {
@mixin dark { @mixin dark {
animation: glowPulse 3s ease-in-out infinite; animation: glowPulse 3s ease-in-out infinite;
border-color: var(--mantine-color-cyan-5); border-color: var(--mantine-color-cyan-5);
color: var(--mantine-color-cyan-5);
} }
} }
&.dev {
border-color: var(--mantine-color-red-4);
color: var(--mantine-color-red-4);
}
} }

View file

@ -1,62 +1,71 @@
import { Group, Text } from '@mantine/core' import { Group, Text } from '@mantine/core'
import { useMemo, useState } from 'react' import { modals } from '@mantine/modals'
import { useMemo } from 'react'
import semver from 'semver' import semver from 'semver'
import clsx from 'clsx' import clsx from 'clsx'
import { getBuildInfo } from '@shared/utils/get-build-info/get-build-info.util'
import { useRemnawaveInfo } from '@entities/dashboard/updates-store' import { useRemnawaveInfo } from '@entities/dashboard/updates-store'
import { useGetRemnawaveMetadata } from '@shared/api/hooks'
import { BaseOverlayHeader } from '../overlays/base-overlay-header'
import { SkeletonHeaderControl } from './SkeletonHeaderControl'
import { BuildInfoModal } from '../sidebar/build-info-modal' import { BuildInfoModal } from '../sidebar/build-info-modal'
import { GameModalShared } from '../sidebar/game-modal'
import packageJson from '../../../../package.json'
import classes from './VersionControl.module.css' import classes from './VersionControl.module.css'
import { HeaderControl } from './HeaderControl' import { HeaderControl } from './HeaderControl'
import { Logo } from '../logo' import { Logo } from '../logo'
export function VersionControl() { export function VersionControl() {
const [buildInfoModalOpened, setBuildInfoModalOpened] = useState(false)
const buildInfo = useMemo(() => getBuildInfo(), [])
const isDev = buildInfo.branch === 'dev'
const remnawaveInfo = useRemnawaveInfo() const remnawaveInfo = useRemnawaveInfo()
const { data: remnawaveMetadata, isLoading } = useGetRemnawaveMetadata()
const isNewVersionAvailable = useMemo(() => { const [isNewVersionAvailable, isDev] = useMemo(() => {
const currentVersion = buildInfo.tag ?? '0.0.0' if (!remnawaveMetadata) return [false, false]
const currentVersion = remnawaveMetadata.version
const latest = remnawaveInfo.latestVersion || '0.0.0' const latest = remnawaveInfo.latestVersion || '0.0.0'
return semver.gt(latest, currentVersion) return [semver.gt(latest, currentVersion), remnawaveMetadata.git.backend.branch === 'dev']
}, [remnawaveInfo.latestVersion, buildInfo.tag]) }, [remnawaveInfo.latestVersion, remnawaveMetadata])
if (isLoading || !remnawaveMetadata) {
return <SkeletonHeaderControl width={85} />
}
const handleClick = () => {
modals.open({
title: (
<BaseOverlayHeader
IconComponent={Logo}
iconVariant="gradient-teal"
title="Build Info"
/>
),
centered: true,
size: 'md',
withCloseButton: true,
children: (
<BuildInfoModal
isNewVersionAvailable={isNewVersionAvailable}
remnawaveMetadata={remnawaveMetadata}
/>
)
})
}
return ( return (
<> <HeaderControl
<HeaderControl className={clsx(classes.version, {
className={clsx(classes.version, { [classes.newVersion]: isNewVersionAvailable && !isDev,
[classes.newVersion]: isNewVersionAvailable && !isDev [classes.dev]: isDev
})} })}
onClick={() => setBuildInfoModalOpened(true)} onClick={handleClick}
w="auto" w="auto"
> >
<Group gap={8} ml={10} mr={10} wrap="nowrap"> <Group gap={8} ml={10} mr={10} wrap="nowrap">
<Logo color={isNewVersionAvailable && !isDev ? 'cyan' : undefined} size={20} /> <Logo size={20} />
<Text <Text ff="text" fw={600} size="sm">
c={isNewVersionAvailable && !isDev ? 'cyan' : undefined} {remnawaveMetadata.version}
ff="text" </Text>
fw={600} </Group>
size="sm" </HeaderControl>
>
{isDev ? 'dev' : packageJson.version}
</Text>
</Group>
</HeaderControl>
<BuildInfoModal
buildInfo={buildInfo}
isNewVersionAvailable={isNewVersionAvailable}
onClose={() => setBuildInfoModalOpened(false)}
opened={buildInfoModalOpened}
/>
<GameModalShared />
</>
) )
} }

View file

@ -4,6 +4,7 @@ export { HeaderControls } from './HeaderControls'
export { LanguageControl } from './LanguageControl' export { LanguageControl } from './LanguageControl'
export { LogoutControl } from './LogoutControl' export { LogoutControl } from './LogoutControl'
export { RefreshControl } from './RefreshControl' export { RefreshControl } from './RefreshControl'
export { SkeletonHeaderControl } from './SkeletonHeaderControl'
export { SupportControl } from './SupportControl' export { SupportControl } from './SupportControl'
export { TelegramControl } from './TelegramControl' export { TelegramControl } from './TelegramControl'
export { VersionControl } from './VersionControl' export { VersionControl } from './VersionControl'

View file

@ -0,0 +1,57 @@
.updateCard {
background: rgba(45, 212, 191, 0.08);
border: 1px solid rgba(45, 212, 191, 0.2);
}
.updateIconBox {
padding: 10px;
border-radius: var(--mantine-radius-md);
background: rgba(45, 212, 191, 0.15);
display: flex;
align-items: center;
justify-content: center;
}
.updateTextWrapper {
flex: 1;
}
.mainCard {
background: linear-gradient(
135deg,
var(--mantine-color-dark-6) 0%,
var(--mantine-color-dark-7) 100%
);
border: 1px solid var(--mantine-color-dark-4);
}
.divider {
opacity: 0.3;
}
.buildTimeCard {
background: rgba(99, 102, 241, 0.08);
border: 1px solid rgba(99, 102, 241, 0.2);
}
.buildNumberCard {
background: rgba(139, 92, 246, 0.08);
border: 1px solid rgba(139, 92, 246, 0.2);
}
.backendCard {
background: rgba(45, 212, 191, 0.06);
border: 1px solid rgba(45, 212, 191, 0.15);
}
.frontendCard {
background: rgba(6, 182, 212, 0.06);
border: 1px solid rgba(6, 182, 212, 0.15);
}
.commitSha {
background: rgba(0, 0, 0, 0.2);
padding: 4px 8px;
border-radius: var(--mantine-radius-sm);
flex: 1;
}

View file

@ -1,20 +1,16 @@
import { import {
ActionIcon,
Badge, Badge,
Box, Box,
Button, Button,
Card, CopyButton,
Code,
Divider, Divider,
Flex,
Group, Group,
Modal,
Paper, Paper,
SimpleGrid,
Stack, Stack,
Text, Text,
ThemeIcon, Tooltip
Title,
Tooltip,
useMantineTheme
} from '@mantine/core' } from '@mantine/core'
import { import {
TbBrandGithub, TbBrandGithub,
@ -23,216 +19,223 @@ import {
TbCheck, TbCheck,
TbCopy, TbCopy,
TbGitBranch, TbGitBranch,
TbHash TbHash,
TbServer,
TbWorld
} from 'react-icons/tb' } from 'react-icons/tb'
import { useClipboard } from '@mantine/hooks' import { GetMetadataCommand } from '@remnawave/backend-contract'
import { IBuildInfo } from '@shared/utils/get-build-info/interfaces/build-info.interface' import { formatTimeUtil } from '@shared/utils/time-utils'
import { CopyableCodeBlock } from '../copyable-code-block'
import classes from './build-info-modal.module.css'
import { Logo } from '../logo' import { Logo } from '../logo'
interface BuildInfoModalProps { interface BuildInfoModalProps {
buildInfo: IBuildInfo
isNewVersionAvailable: boolean isNewVersionAvailable: boolean
onClose: () => void remnawaveMetadata: GetMetadataCommand.Response['response']
opened: boolean
} }
export function BuildInfoModal({ export function BuildInfoModal({ remnawaveMetadata, isNewVersionAvailable }: BuildInfoModalProps) {
opened,
onClose,
buildInfo,
isNewVersionAvailable
}: BuildInfoModalProps) {
const buildDate = new Date(buildInfo.buildTime).toLocaleString()
const clipboard = useClipboard({ timeout: 1000 })
const theme = useMantineTheme()
const copyBuildInfo = () => {
clipboard.copy(JSON.stringify(buildInfo, null, 2))
}
return ( return (
<Modal <Stack gap="md">
centered {isNewVersionAvailable && (
onClose={onClose} <Paper className={classes.updateCard} p="md" radius="md">
opened={opened} <Group align="center" gap="md" wrap="wrap">
padding="xl" <Group gap="sm" wrap="nowrap">
title={ <Box className={classes.updateIconBox}>
<Group justify="space-between" w="100%"> <Logo color="var(--mantine-color-teal-4)" size={24} />
<Title c={theme.primaryColor} fw={700} order={3}> </Box>
Build Info <Stack className={classes.updateTextWrapper} gap={4}>
</Title> <Text c="teal.4" fw={600} size="sm">
<Tooltip label={clipboard.copied ? 'Copied!' : 'Copy build info'}>
<Button
color={clipboard.copied ? 'green' : 'gray'}
leftSection={
clipboard.copied ? <TbCheck size={16} /> : <TbCopy size={16} />
}
onClick={copyBuildInfo}
radius="xl"
size="compact-sm"
variant="light"
>
Copy
</Button>
</Tooltip>
</Group>
}
withCloseButton
>
<Stack gap="xl">
{isNewVersionAvailable && (
<Paper
bg="rgba(0, 180, 160, 0.05)"
p="lg"
style={{ border: `1px solid ${theme.colors.teal[3]}` }}
withBorder
>
<Group align="flex-start" gap="md">
<ThemeIcon color="cyan" radius="xl" size={48} variant="outline">
<Logo size={24} />
</ThemeIcon>
<Stack gap="xs" style={{ flex: 1 }}>
<Text c="teal.5" fw={700} size="md">
Update available Update available
</Text> </Text>
<Text c="dimmed" size="md"> <Text c="dimmed" size="xs">
A new version is available. A new version is available
</Text> </Text>
<Button
color="teal"
component="a"
fullWidth={false}
href="https://t.me/remnalog"
leftSection={<TbBrandTelegram size={16} />}
mt="sm"
size="sm"
style={{ alignSelf: 'flex-start' }}
target="_blank"
>
Check out
</Button>
</Stack> </Stack>
</Group> </Group>
</Paper>
)}
<Card padding="lg" shadow="sm" withBorder> <Button
<Stack gap="lg"> color="teal"
<Group align="center" gap="lg"> component="a"
<ThemeIcon href="https://t.me/remnalog"
color={theme.primaryColor} leftSection={<TbBrandTelegram size={14} />}
radius="xl" ml="auto"
size={48} radius="md"
style={{ size="xs"
border: `1px solid ${theme.colors[theme.primaryColor][5]}` target="_blank"
}} variant="light"
>
Check out
</Button>
</Group>
</Paper>
)}
<Paper className={classes.mainCard} p="md">
<Stack gap="md">
<Group justify="space-between">
<Group gap="sm">
<Badge
color="cyan"
leftSection={<Logo size={16} />}
size="lg"
variant="light" variant="light"
> >
<TbCalendar size={24} /> {remnawaveMetadata.version}
</ThemeIcon> </Badge>
<Box style={{ flex: 1 }}>
<Text fw={700} size="md"> <Badge
color={
remnawaveMetadata.git.backend.branch === 'dev' ? 'red' : 'teal'
}
leftSection={<TbGitBranch size={16} />}
size="lg"
variant="light"
>
{remnawaveMetadata.git.backend.branch}
</Badge>
</Group>
<CopyButton
timeout={2000}
value={JSON.stringify(remnawaveMetadata, null, 2)}
>
{({ copied, copy }) => (
<Tooltip label="Copy build info">
<ActionIcon
color={copied ? 'teal' : 'gray'}
onClick={copy}
size="md"
variant="subtle"
>
{copied ? <TbCheck size={14} /> : <TbCopy size={14} />}
</ActionIcon>
</Tooltip>
)}
</CopyButton>
</Group>
<Divider className={classes.divider} />
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="sm">
<Paper className={classes.buildTimeCard} p="sm" radius="md">
<Group gap="xs" mb={6}>
<TbCalendar color="var(--mantine-color-indigo-5)" size={14} />
<Text c="indigo.5" fw={600} size="xs" tt="uppercase">
Build Time Build Time
</Text> </Text>
<Text c="dimmed" mt={4} size="sm"> </Group>
{buildDate} <Text c="gray.3" ff="monospace" size="xs">
{formatTimeUtil(
remnawaveMetadata.build.time,
'DD.MM.YYYY HH:mm:ss'
)}
</Text>
</Paper>
<Paper className={classes.buildNumberCard} p="sm" radius="md">
<Group gap="xs" mb={6}>
<TbHash color="var(--mantine-color-violet-5)" size={14} />
<Text c="violet.5" fw={600} size="xs" tt="uppercase">
Build
</Text> </Text>
</Box> </Group>
<Text c="gray.3" ff="monospace" size="xs">
{remnawaveMetadata.build.number}
</Text>
</Paper>
</SimpleGrid>
</Stack>
</Paper>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="sm">
<Paper className={classes.backendCard} p="md" radius="md">
<Stack gap="sm">
<Group gap="xs" justify="space-between">
<Group gap="xs">
<TbServer color="var(--mantine-color-teal-5)" size={16} />
<Text c="teal.5" fw={600} size="sm">
Backend
</Text>
</Group>
<Tooltip label="View on GitHub">
<ActionIcon
color="teal"
component="a"
href={remnawaveMetadata.git.backend.commitUrl}
size="sm"
target="_blank"
variant="subtle"
>
<TbBrandGithub size={14} />
</ActionIcon>
</Tooltip>
</Group> </Group>
<Divider variant="dashed" /> <CopyableCodeBlock
size="small"
<Group align="center" gap="lg"> value={remnawaveMetadata.git.backend.commitSha}
<ThemeIcon />
color={theme.primaryColor}
radius="xl"
size={48}
style={{
border: `1px solid ${theme.colors[theme.primaryColor][5]}`
}}
variant="light"
>
<TbGitBranch size={24} />
</ThemeIcon>
<Box style={{ flex: 1 }}>
<Text fw={700} size="md">
Branch
</Text>
<Flex gap="xs" mt={6}>
<Badge
color="blue"
px="md"
radius="xl"
size="lg"
variant="light"
>
{buildInfo.branch}
</Badge>
{buildInfo.tag && (
<Badge
color="green"
px="md"
radius="xl"
size="lg"
variant="light"
>
{buildInfo.tag}
</Badge>
)}
</Flex>
</Box>
</Group>
<Divider variant="dashed" />
<Group align="center" gap="lg">
<ThemeIcon
color={theme.primaryColor}
radius="xl"
size={48}
style={{
border: `1px solid ${theme.colors[theme.primaryColor][5]}`
}}
variant="light"
>
<TbHash size={24} />
</ThemeIcon>
<Box style={{ flex: 1 }}>
<Text fw={700} size="md">
Commit
</Text>
<Code fz="sm">{buildInfo.commit}</Code>
</Box>
</Group>
</Stack> </Stack>
</Card> </Paper>
<Group gap="md" grow preventGrowOverflow={false} wrap="wrap"> <Paper className={classes.frontendCard} p="md" radius="md">
<Button <Stack gap="sm">
component="a" <Group gap="xs" justify="space-between">
href={buildInfo.commitUrl} <Group gap="xs">
leftSection={<TbBrandGithub size={18} />} <TbWorld color="var(--mantine-color-cyan-5)" size={16} />
size="md" <Text c="cyan.5" fw={600} size="sm">
target="_blank" Frontend
variant="outline" </Text>
> </Group>
View on GitHub <Tooltip label="View on GitHub">
</Button> <ActionIcon
color="cyan"
component="a"
href={remnawaveMetadata.git.frontend.commitUrl}
size="sm"
target="_blank"
variant="subtle"
>
<TbBrandGithub size={14} />
</ActionIcon>
</Tooltip>
</Group>
<Button <CopyableCodeBlock
color="cyan" size="small"
component="a" value={remnawaveMetadata.git.frontend.commitSha}
href="https://t.me/remnawave" />
leftSection={<TbBrandTelegram size={18} />} </Stack>
size="md" </Paper>
target="_blank" </SimpleGrid>
>
Ask Community <Group gap="sm" grow>
</Button> <Button
</Group> color="cyan"
</Stack> component="a"
</Modal> href="https://t.me/remnawave"
leftSection={<TbBrandTelegram size={16} />}
radius="md"
size="sm"
target="_blank"
variant="light"
>
Community
</Button>
<Button
component="a"
href="https://github.com/remnawave"
leftSection={<TbBrandGithub size={16} />}
radius="md"
size="sm"
target="_blank"
variant="default"
>
GitHub
</Button>
</Group>
</Stack>
) )
} }

View file

@ -1,6 +0,0 @@
import { IBuildInfo } from './interfaces/build-info.interface'
import buildInfo from '../../../../build.info.json'
export function getBuildInfo(): IBuildInfo {
return buildInfo
}

View file

@ -1,8 +0,0 @@
export interface IBuildInfo {
branch: string
buildTime: string
commit: string
commitFull: string
commitUrl: string
tag: null | string
}

View file

@ -1,6 +1,6 @@
import dayjs from 'dayjs' import dayjs from 'dayjs'
type TTemplatePreset = 'D MMM' | 'D MMMM YYYY' type TTemplatePreset = 'DD.MM.YYYY HH:mm:ss' | 'D MMM' | 'D MMMM YYYY'
export const formatTimeUtil = ( export const formatTimeUtil = (
time: null | number | string | undefined, time: null | number | string | undefined,