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:
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
run: |
npm ci

View file

@ -40,33 +40,6 @@ jobs:
with:
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
id: changelog
run: |

2
.gitignore vendored
View file

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

8
package-lock.json generated
View file

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

View file

@ -59,7 +59,7 @@
"@monaco-editor/react": "^4.7.0",
"@noble/post-quantum": "^0.5.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",
"@simplewebauthn/browser": "^13.2.2",
"@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 { HeaderControls } from '@shared/ui/header-buttons'
import { HelpDrawerShared } from '@shared/ui/help-drawer'
import { GameModalShared } from '@shared/ui/sidebar'
import { Navigation } from './navbar/navigation.layout'
import classes from './Main.module.css'
@ -118,6 +119,7 @@ export function MainLayout() {
</AppShell.Section>
<AppShell.Section className={classes.footerSection}>
<GameModalShared />
{isSocialButton && (
<Group justify="center" mt="md" style={{ flexShrink: 0 }}>
<HeaderControls

View file

@ -1,5 +1,6 @@
import {
GetBandwidthStatsCommand,
GetMetadataCommand,
GetNodesMetricsCommand,
GetNodesStatisticsCommand,
GetRemnawaveHealthCommand,
@ -30,6 +31,9 @@ export const systemQueryKeys = createQueryKeys('system', {
},
getNodesMetrics: {
queryKey: null
},
getRemnawaveMetadata: {
queryKey: null
}
})
@ -104,3 +108,15 @@ export const useGetNodesMetrics = createGetQueryHook({
},
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;
}
.container.small {
gap: 8px;
padding: 8px 12px;
font-size: var(--mantine-font-size-xs);
border-radius: var(--mantine-radius-sm);
}
.container:hover {
border-color: var(--mantine-color-dark-4);
}
@ -36,6 +43,10 @@
z-index: 1;
}
.small .codeWrapper::after {
width: 24px;
}
.code {
overflow-x: auto;
white-space: nowrap;

View file

@ -1,27 +1,37 @@
import { ActionIcon, Box, CopyButton } from '@mantine/core'
import { PiCheck, PiCopy } from 'react-icons/pi'
import clsx from 'clsx'
import styles from './copyable-code-block.module.css'
interface IProps {
size?: 'normal' | 'small'
value: string
}
export function CopyableCodeBlock({ value }: IProps) {
export function CopyableCodeBlock({ value, size = 'normal' }: IProps) {
const isSmall = size === 'small'
const iconSize = isSmall ? 14 : 18
return (
<CopyButton timeout={2000} value={value}>
{({ 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.code}>{value}</Box>
</Box>
<ActionIcon
className={styles.copyButton}
data-copied={copied}
size="sm"
size={isSmall ? 'xs' : 'sm'}
variant="transparent"
>
{copied ? <PiCheck size={18} /> : <PiCopy size={18} />}
{copied ? <PiCheck size={iconSize} /> : <PiCopy size={iconSize} />}
</ActionIcon>
</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 {
cursor: pointer;
min-width: 85px;
&.newVersion {
@mixin dark {
animation: glowPulse 3s ease-in-out infinite;
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 { useMemo, useState } from 'react'
import { modals } from '@mantine/modals'
import { useMemo } from 'react'
import semver from 'semver'
import clsx from 'clsx'
import { getBuildInfo } from '@shared/utils/get-build-info/get-build-info.util'
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 { GameModalShared } from '../sidebar/game-modal'
import packageJson from '../../../../package.json'
import classes from './VersionControl.module.css'
import { HeaderControl } from './HeaderControl'
import { Logo } from '../logo'
export function VersionControl() {
const [buildInfoModalOpened, setBuildInfoModalOpened] = useState(false)
const buildInfo = useMemo(() => getBuildInfo(), [])
const isDev = buildInfo.branch === 'dev'
const remnawaveInfo = useRemnawaveInfo()
const { data: remnawaveMetadata, isLoading } = useGetRemnawaveMetadata()
const isNewVersionAvailable = useMemo(() => {
const currentVersion = buildInfo.tag ?? '0.0.0'
const [isNewVersionAvailable, isDev] = useMemo(() => {
if (!remnawaveMetadata) return [false, false]
const currentVersion = remnawaveMetadata.version
const latest = remnawaveInfo.latestVersion || '0.0.0'
return semver.gt(latest, currentVersion)
}, [remnawaveInfo.latestVersion, buildInfo.tag])
return [semver.gt(latest, currentVersion), remnawaveMetadata.git.backend.branch === 'dev']
}, [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 (
<>
<HeaderControl
className={clsx(classes.version, {
[classes.newVersion]: isNewVersionAvailable && !isDev
[classes.newVersion]: isNewVersionAvailable && !isDev,
[classes.dev]: isDev
})}
onClick={() => setBuildInfoModalOpened(true)}
onClick={handleClick}
w="auto"
>
<Group gap={8} ml={10} mr={10} wrap="nowrap">
<Logo color={isNewVersionAvailable && !isDev ? 'cyan' : undefined} size={20} />
<Text
c={isNewVersionAvailable && !isDev ? 'cyan' : undefined}
ff="text"
fw={600}
size="sm"
>
{isDev ? 'dev' : packageJson.version}
<Logo size={20} />
<Text ff="text" fw={600} size="sm">
{remnawaveMetadata.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 { LogoutControl } from './LogoutControl'
export { RefreshControl } from './RefreshControl'
export { SkeletonHeaderControl } from './SkeletonHeaderControl'
export { SupportControl } from './SupportControl'
export { TelegramControl } from './TelegramControl'
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 {
ActionIcon,
Badge,
Box,
Button,
Card,
Code,
CopyButton,
Divider,
Flex,
Group,
Modal,
Paper,
SimpleGrid,
Stack,
Text,
ThemeIcon,
Title,
Tooltip,
useMantineTheme
Tooltip
} from '@mantine/core'
import {
TbBrandGithub,
@ -23,216 +19,223 @@ import {
TbCheck,
TbCopy,
TbGitBranch,
TbHash
TbHash,
TbServer,
TbWorld
} 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'
interface BuildInfoModalProps {
buildInfo: IBuildInfo
isNewVersionAvailable: boolean
onClose: () => void
opened: boolean
remnawaveMetadata: GetMetadataCommand.Response['response']
}
export function BuildInfoModal({
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))
}
export function BuildInfoModal({ remnawaveMetadata, isNewVersionAvailable }: BuildInfoModalProps) {
return (
<Modal
centered
onClose={onClose}
opened={opened}
padding="xl"
title={
<Group justify="space-between" w="100%">
<Title c={theme.primaryColor} fw={700} order={3}>
Build Info
</Title>
<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">
<Stack gap="md">
{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">
<Paper className={classes.updateCard} p="md" radius="md">
<Group align="center" gap="md" wrap="wrap">
<Group gap="sm" wrap="nowrap">
<Box className={classes.updateIconBox}>
<Logo color="var(--mantine-color-teal-4)" size={24} />
</Box>
<Stack className={classes.updateTextWrapper} gap={4}>
<Text c="teal.4" fw={600} size="sm">
Update available
</Text>
<Text c="dimmed" size="md">
A new version is available.
<Text c="dimmed" size="xs">
A new version is available
</Text>
</Stack>
</Group>
<Button
color="teal"
component="a"
fullWidth={false}
href="https://t.me/remnalog"
leftSection={<TbBrandTelegram size={16} />}
mt="sm"
size="sm"
style={{ alignSelf: 'flex-start' }}
leftSection={<TbBrandTelegram size={14} />}
ml="auto"
radius="md"
size="xs"
target="_blank"
variant="light"
>
Check out
</Button>
</Stack>
</Group>
</Paper>
)}
<Card padding="lg" shadow="sm" withBorder>
<Stack gap="lg">
<Group align="center" gap="lg">
<ThemeIcon
color={theme.primaryColor}
radius="xl"
size={48}
style={{
border: `1px solid ${theme.colors[theme.primaryColor][5]}`
}}
<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"
>
<TbCalendar size={24} />
</ThemeIcon>
<Box style={{ flex: 1 }}>
<Text fw={700} size="md">
{remnawaveMetadata.version}
</Badge>
<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
</Text>
<Text c="dimmed" mt={4} size="sm">
{buildDate}
</Text>
</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"
>
<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>
<Text c="gray.3" ff="monospace" size="xs">
{formatTimeUtil(
remnawaveMetadata.build.time,
'DD.MM.YYYY HH:mm:ss'
)}
</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>
</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>
</Group>
<Text c="gray.3" ff="monospace" size="xs">
{remnawaveMetadata.build.number}
</Text>
</Paper>
</SimpleGrid>
</Stack>
</Card>
</Paper>
<Group gap="md" grow preventGrowOverflow={false} wrap="wrap">
<Button
<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={buildInfo.commitUrl}
leftSection={<TbBrandGithub size={18} />}
size="md"
href={remnawaveMetadata.git.backend.commitUrl}
size="sm"
target="_blank"
variant="outline"
variant="subtle"
>
View on GitHub
</Button>
<TbBrandGithub size={14} />
</ActionIcon>
</Tooltip>
</Group>
<CopyableCodeBlock
size="small"
value={remnawaveMetadata.git.backend.commitSha}
/>
</Stack>
</Paper>
<Paper className={classes.frontendCard} p="md" radius="md">
<Stack gap="sm">
<Group gap="xs" justify="space-between">
<Group gap="xs">
<TbWorld color="var(--mantine-color-cyan-5)" size={16} />
<Text c="cyan.5" fw={600} size="sm">
Frontend
</Text>
</Group>
<Tooltip label="View on GitHub">
<ActionIcon
color="cyan"
component="a"
href={remnawaveMetadata.git.frontend.commitUrl}
size="sm"
target="_blank"
variant="subtle"
>
<TbBrandGithub size={14} />
</ActionIcon>
</Tooltip>
</Group>
<CopyableCodeBlock
size="small"
value={remnawaveMetadata.git.frontend.commitSha}
/>
</Stack>
</Paper>
</SimpleGrid>
<Group gap="sm" grow>
<Button
color="cyan"
component="a"
href="https://t.me/remnawave"
leftSection={<TbBrandTelegram size={18} />}
size="md"
leftSection={<TbBrandTelegram size={16} />}
radius="md"
size="sm"
target="_blank"
variant="light"
>
Ask Community
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>
</Modal>
)
}

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'
type TTemplatePreset = 'D MMM' | 'D MMMM YYYY'
type TTemplatePreset = 'DD.MM.YYYY HH:mm:ss' | 'D MMM' | 'D MMMM YYYY'
export const formatTimeUtil = (
time: null | number | string | undefined,