mirror of
https://github.com/remnawave/frontend.git
synced 2026-05-13 04:09:03 +00:00
refactor: build info modal
This commit is contained in:
parent
0db4f67329
commit
f934f2dce9
19 changed files with 407 additions and 312 deletions
23
.github/workflows/release-dev.yml
vendored
23
.github/workflows/release-dev.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
27
.github/workflows/release-frontend.yml
vendored
27
.github/workflows/release-frontend.yml
vendored
|
|
@ -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
2
.gitignore
vendored
|
|
@ -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
8
package-lock.json
generated
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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%);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/shared/ui/header-buttons/SkeletonHeaderControl.tsx
Normal file
11
src/shared/ui/header-buttons/SkeletonHeaderControl.tsx
Normal 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} />
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
57
src/shared/ui/sidebar/build-info-modal.module.css
Normal file
57
src/shared/ui/sidebar/build-info-modal.module.css
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
import { IBuildInfo } from './interfaces/build-info.interface'
|
|
||||||
import buildInfo from '../../../../build.info.json'
|
|
||||||
|
|
||||||
export function getBuildInfo(): IBuildInfo {
|
|
||||||
return buildInfo
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
export interface IBuildInfo {
|
|
||||||
branch: string
|
|
||||||
buildTime: string
|
|
||||||
commit: string
|
|
||||||
commitFull: string
|
|
||||||
commitUrl: string
|
|
||||||
tag: null | string
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue