mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
feat: dev tools
This commit is contained in:
1 parent
74b821ac13
commit
edce64c1bd
24 files changed
+698
-93
No files matched your search
@@ -1,13 +1,11 @@
|
||||
import { Flex, type FlexProps, Theme } from '@radix-ui/themes';
|
||||
import type { ComponentProps } from 'react';
|
||||
import type { RadixSize } from '../stores/embedState';
|
||||
import { BlitzKitTheme } from './BlitzKitTheme';
|
||||
|
||||
type PageWrapperProps = FlexProps & {
|
||||
color?: ComponentProps<typeof Theme>['accentColor'];
|
||||
containerProps?: ComponentProps<typeof Theme>;
|
||||
noFlex1?: boolean;
|
||||
padding?: RadixSize;
|
||||
noMinHeight?: boolean;
|
||||
};
|
||||
|
||||
@@ -18,7 +16,6 @@ export function PageWrapper({
|
||||
noFlex1 = false,
|
||||
containerProps,
|
||||
noMinHeight = false,
|
||||
padding = '4',
|
||||
...props
|
||||
}: PageWrapperProps) {
|
||||
return (
|
||||
@@ -35,8 +32,9 @@ export function PageWrapper({
|
||||
>
|
||||
<Flex
|
||||
direction="column"
|
||||
gap="4"
|
||||
p={padding}
|
||||
gap="8"
|
||||
px="4"
|
||||
py="8"
|
||||
width="100%"
|
||||
maxWidth="80rem"
|
||||
style={{
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { fetchMetadata } from '@blitzkit/closed';
|
||||
import { BlitzkitServerAPI, MetadataAccessor } from '@blitzkit/core';
|
||||
import {
|
||||
assertSecret,
|
||||
BlitzkitServerAPI,
|
||||
MetadataAccessor,
|
||||
} from '@blitzkit/core';
|
||||
|
||||
const metadata = await fetchMetadata();
|
||||
const metadata = await fetchMetadata(
|
||||
assertSecret(import.meta.env.AUTO_PROXY_ORIGIN),
|
||||
);
|
||||
const accessor = new MetadataAccessor(metadata);
|
||||
export const api = new BlitzkitServerAPI(accessor);
|
||||
@@ -8,6 +8,8 @@ export const ffi = Library(FFI_PATH, {
|
||||
debug_echo: [types.CString, [types.CString]],
|
||||
mount_game: ['void', [types.CString]],
|
||||
debug_get_file_count: [types.int, []],
|
||||
get_bindings_url_production: [types.CString, []],
|
||||
get_bindings_url_dev: [types.CString, []],
|
||||
});
|
||||
|
||||
ffi.mount_game(assertSecret(import.meta.env.GAME_MOUNT_POINT));
|
||||
@@ -1,14 +1,49 @@
|
||||
import { Heading } from '@radix-ui/themes';
|
||||
import { FFIEcho } from '../../components/Internal/FFIEcho';
|
||||
import { FFIFileCount } from '../../components/Internal/FFIFileCount';
|
||||
import { Link, Table } from '@radix-ui/themes';
|
||||
import { PageWrapper } from '../../components/PageWrapper';
|
||||
|
||||
export function Page() {
|
||||
return (
|
||||
<PageWrapper color="amber">
|
||||
<Heading>BlitzKit FFI</Heading>
|
||||
<FFIEcho />
|
||||
<FFIFileCount />
|
||||
<PageWrapper align="center">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.ColumnHeaderCell>Path</Table.ColumnHeaderCell>
|
||||
<Table.ColumnHeaderCell>Memo</Table.ColumnHeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
|
||||
<Table.Body>
|
||||
<Table.Row>
|
||||
<Table.RowHeaderCell>
|
||||
<Link href="/internal/debug">Debug</Link>
|
||||
</Table.RowHeaderCell>
|
||||
<Table.Cell>
|
||||
Some debug information to check if everything is up
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
|
||||
<Table.Row>
|
||||
<Table.RowHeaderCell>
|
||||
<Link href="/internal/bindings">Bindings</Link>
|
||||
</Table.RowHeaderCell>
|
||||
<Table.Cell>List of all active bindings</Table.Cell>
|
||||
</Table.Row>
|
||||
|
||||
<Table.Row>
|
||||
<Table.RowHeaderCell>
|
||||
<Link href="/internal/discovery">Discovery</Link>
|
||||
</Table.RowHeaderCell>
|
||||
<Table.Cell>List of all active discovery lists</Table.Cell>
|
||||
</Table.Row>
|
||||
|
||||
<Table.Row>
|
||||
<Table.RowHeaderCell>
|
||||
<Link href="/internal/differ">Differ</Link>
|
||||
</Table.RowHeaderCell>
|
||||
<Table.Cell>Differentiate two branches</Table.Cell>
|
||||
</Table.Row>
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Badge, Flex, Heading, Link, Separator, Text } from '@radix-ui/themes';
|
||||
import { PageWrapper } from 'packages/website-ue/src/components/PageWrapper';
|
||||
import { ffi } from 'packages/website-ue/src/core/blitzkit/ffi';
|
||||
|
||||
export interface Bindings {
|
||||
bindings: Binding[];
|
||||
forbidden: string[];
|
||||
}
|
||||
|
||||
interface Binding {
|
||||
name: string;
|
||||
versions: string[];
|
||||
discovery: string;
|
||||
env: string;
|
||||
show_server_selection: boolean;
|
||||
}
|
||||
|
||||
export async function Page() {
|
||||
return (
|
||||
<PageWrapper>
|
||||
<Bindings url={JSON.parse(ffi.get_bindings_url_dev()!)} dev />
|
||||
<Separator size="4" />
|
||||
<Bindings url={JSON.parse(ffi.get_bindings_url_production()!)} />
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
async function Bindings({ url, dev }: { url: string; dev?: boolean }) {
|
||||
const data = await fetch(url).then(
|
||||
(response) => response.json() as Promise<Bindings>,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text>
|
||||
Bindings URL{' '}
|
||||
<Link href={url} target="_blank">
|
||||
{url}
|
||||
</Link>
|
||||
</Text>
|
||||
|
||||
{data.bindings.map((binding) => (
|
||||
<Flex direction="column" gap="2" key={binding.name}>
|
||||
<Heading>
|
||||
<Flex align="center" gap="2">
|
||||
{binding.env}
|
||||
|
||||
{!dev && (
|
||||
<Badge variant="solid" color="blue">
|
||||
Production
|
||||
</Badge>
|
||||
)}
|
||||
{dev && (
|
||||
<Badge variant="solid" color="orange">
|
||||
Dev
|
||||
</Badge>
|
||||
)}
|
||||
{binding.show_server_selection && (
|
||||
<Badge color="green">Public</Badge>
|
||||
)}
|
||||
{!binding.show_server_selection && (
|
||||
<Badge color="red">Private</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
</Heading>
|
||||
|
||||
<Text>{binding.name}</Text>
|
||||
|
||||
<Flex gap="2">
|
||||
<Text>Discovery:</Text>
|
||||
<Link href={binding.discovery} target="_blank">
|
||||
{binding.discovery}
|
||||
</Link>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="2">
|
||||
<Text>Versions:</Text>
|
||||
|
||||
{binding.versions.map((version) => (
|
||||
<Badge variant="outline" color="gray" key={version}>
|
||||
{version}
|
||||
</Badge>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import InternalBaseLayout from 'packages/website-ue/src/layouts/InternalBaseLayout.astro';
|
||||
import { Page } from './_index';
|
||||
---
|
||||
|
||||
<InternalBaseLayout>
|
||||
<Page />
|
||||
</InternalBaseLayout>
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Heading } from '@radix-ui/themes';
|
||||
import { FFIEcho } from '../../../components/Internal/FFIEcho';
|
||||
import { FFIFileCount } from '../../../components/Internal/FFIFileCount';
|
||||
import { PageWrapper } from '../../../components/PageWrapper';
|
||||
|
||||
export function Page() {
|
||||
return (
|
||||
<PageWrapper color="amber">
|
||||
<Heading>BlitzKit FFI</Heading>
|
||||
<FFIEcho />
|
||||
<FFIFileCount />
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import InternalBaseLayout from 'packages/website-ue/src/layouts/InternalBaseLayout.astro';
|
||||
import { Page } from './_index';
|
||||
---
|
||||
|
||||
<InternalBaseLayout>
|
||||
<Page />
|
||||
</InternalBaseLayout>
|
||||
@@ -0,0 +1,289 @@
|
||||
import type { MessageFns } from '@protos/blitz_static_profile_avatar_component';
|
||||
import type { Any } from '@protos/google/protobuf/any';
|
||||
import * as protos from '@protos/index';
|
||||
import { CommitIcon, ReaderIcon } from '@radix-ui/react-icons';
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Code,
|
||||
Flex,
|
||||
IconButton,
|
||||
Select,
|
||||
Spinner,
|
||||
Text,
|
||||
} from '@radix-ui/themes';
|
||||
import { MetadataAccessor } from 'packages/core/src';
|
||||
import type { CatalogItemAccessor } from 'packages/core/src/blitz/catalogItemAccessor';
|
||||
import { PageWrapper } from 'packages/website-ue/src/components/PageWrapper';
|
||||
import { Var } from 'packages/website-ue/src/core/radix/var';
|
||||
import { useState } from 'react';
|
||||
import { fetchMetadata } from 'submodules/blitzkit-closed/src';
|
||||
import type { DiscoveryServer } from '../discovery/_index';
|
||||
|
||||
export function Page({ servers }: { servers: DiscoveryServer[] }) {
|
||||
const [serverA, setSeverA] = useState<DiscoveryServer | undefined>(undefined);
|
||||
const [serverB, setSeverB] = useState<DiscoveryServer | undefined>(undefined);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [diff, setDiff] = useState<CatalogDiffEntry[] | null>(null);
|
||||
|
||||
return (
|
||||
<PageWrapper justify="center">
|
||||
<Flex justify="center" align="center" gap="2">
|
||||
<Select.Root
|
||||
size="3"
|
||||
defaultValue="undefined"
|
||||
onValueChange={(value) => {
|
||||
setSeverA(servers.find((server) => server.name === value));
|
||||
}}
|
||||
>
|
||||
<Select.Trigger style={{ flex: 1, maxWidth: '16rem' }} />
|
||||
<Select.Content>
|
||||
<Select.Item value="undefined">Select base server</Select.Item>
|
||||
|
||||
{servers.map((server) => (
|
||||
<Select.Item key={server.name} value={server.name}>
|
||||
{server.name}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
|
||||
<IconButton
|
||||
size="3"
|
||||
disabled={!serverA || !serverB}
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
|
||||
const diff = await diffServers(serverA!.addr, serverB!.addr);
|
||||
|
||||
setLoading(false);
|
||||
setDiff(diff);
|
||||
}}
|
||||
>
|
||||
{loading ? <Spinner /> : <CommitIcon />}
|
||||
</IconButton>
|
||||
|
||||
<Select.Root
|
||||
size="3"
|
||||
defaultValue="undefined"
|
||||
onValueChange={(value) => {
|
||||
setSeverB(servers.find((server) => server.name === value));
|
||||
}}
|
||||
>
|
||||
<Select.Trigger style={{ flex: 1, maxWidth: '16rem' }} />
|
||||
<Select.Content>
|
||||
<Select.Item value="undefined">Select base server</Select.Item>
|
||||
|
||||
{servers.map((server) => (
|
||||
<Select.Item key={server.name} value={server.name}>
|
||||
{server.name}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</Flex>
|
||||
|
||||
{diff !== null && (
|
||||
<DiffDisplay
|
||||
diff={diff.filter(
|
||||
(entry) => entry.type !== CatalogItemDiffType.Unmodified,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
function DiffDisplay({ diff }: { diff: CatalogDiffEntry[] }) {
|
||||
return (
|
||||
<Flex direction="column" gap="4">
|
||||
{diff.length === 0 && (
|
||||
<Text align="center" color="gray">
|
||||
Branches are identical
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{diff.map((entry) => (
|
||||
<Flex
|
||||
direction="column"
|
||||
gap="2"
|
||||
style={{
|
||||
backgroundColor: Var('gray-3'),
|
||||
boxShadow: Var('shadow-3'),
|
||||
borderRadius: Var('radius-3'),
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Box p="3">
|
||||
<Text>
|
||||
{diffEmojis[entry.type]} {entry.name}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Flex gap="1">
|
||||
{(entry.type === CatalogItemDiffType.Removed ||
|
||||
entry.type === CatalogItemDiffType.Unmodified) && (
|
||||
<ItemDisplay item={entry.item} />
|
||||
)}
|
||||
{entry.type === CatalogItemDiffType.Modified && (
|
||||
<ItemDisplay item={entry.a} />
|
||||
)}
|
||||
{(entry.type === CatalogItemDiffType.Added ||
|
||||
entry.type === CatalogItemDiffType.Unmodified) && (
|
||||
<ItemDisplay item={entry.item} />
|
||||
)}
|
||||
{entry.type === CatalogItemDiffType.Modified && (
|
||||
<ItemDisplay item={entry.b} />
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemDisplay({ item }: { item: CatalogItemAccessor }) {
|
||||
const entries = Object.entries(item.components);
|
||||
|
||||
protos;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
direction="column"
|
||||
gap="4"
|
||||
flexGrow="1"
|
||||
p="3"
|
||||
style={{
|
||||
backgroundColor: Var('gray-2'),
|
||||
}}
|
||||
>
|
||||
{entries.length === 0 && <Text color="gray">No components</Text>}
|
||||
|
||||
{entries.map(([key, value]) => (
|
||||
<ComponentDisplay key={key} name={key} value={value} />
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
function ComponentDisplay({ name, value }: { name: string; value: Any }) {
|
||||
const [code, setCode] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="1">
|
||||
<Flex gap="2" align="center">
|
||||
<IconButton
|
||||
size="1"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
const messageName = value.type_url
|
||||
.slice(2)
|
||||
.split('.')
|
||||
.slice(1)
|
||||
.join('.');
|
||||
const json = (
|
||||
protos[messageName as keyof typeof protos] as MessageFns<any>
|
||||
).decode(value.value);
|
||||
|
||||
setCode(JSON.stringify(json, null, 2));
|
||||
}}
|
||||
>
|
||||
<ReaderIcon />
|
||||
</IconButton>
|
||||
|
||||
<Code color="gray" highContrast variant="ghost">
|
||||
{name}
|
||||
</Code>
|
||||
<Badge>{value.value.length.toLocaleString()}B</Badge>
|
||||
</Flex>
|
||||
|
||||
{code !== null && (
|
||||
<Code
|
||||
ml="4"
|
||||
size="1"
|
||||
color="gray"
|
||||
highContrast
|
||||
style={{ whiteSpace: 'pre' }}
|
||||
>
|
||||
{code}
|
||||
</Code>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
enum CatalogItemDiffType {
|
||||
Unmodified,
|
||||
Modified,
|
||||
Added,
|
||||
Removed,
|
||||
}
|
||||
|
||||
type CatalogDiffEntry = { name: string } & (
|
||||
| {
|
||||
type: CatalogItemDiffType.Modified;
|
||||
a: CatalogItemAccessor;
|
||||
b: CatalogItemAccessor;
|
||||
}
|
||||
| {
|
||||
type: Exclude<CatalogItemDiffType, CatalogItemDiffType.Modified>;
|
||||
item: CatalogItemAccessor;
|
||||
}
|
||||
);
|
||||
|
||||
const diffEmojis: Record<CatalogItemDiffType, string> = {
|
||||
[CatalogItemDiffType.Unmodified]: '🩶',
|
||||
[CatalogItemDiffType.Modified]: '🟡',
|
||||
[CatalogItemDiffType.Added]: '🟢',
|
||||
[CatalogItemDiffType.Removed]: '🔴',
|
||||
};
|
||||
|
||||
async function diffServers(urlA: string, urlB: string) {
|
||||
const [metadataA, metadataB] = await Promise.all([
|
||||
fetchMetadata(urlA),
|
||||
fetchMetadata(urlB),
|
||||
]).then((members) =>
|
||||
members.map((metadata) => new MetadataAccessor(metadata)),
|
||||
);
|
||||
const itemNameSet = new Set<string>();
|
||||
|
||||
for (const item in metadataA.items) itemNameSet.add(item);
|
||||
for (const item in metadataB.items) itemNameSet.add(item);
|
||||
|
||||
const itemNames = [...itemNameSet].sort();
|
||||
const diff: CatalogDiffEntry[] = [];
|
||||
|
||||
for (const name of itemNames) {
|
||||
if (name in metadataA.items) {
|
||||
if (name in metadataB.items) {
|
||||
const a = metadataA.get(name);
|
||||
const b = metadataB.get(name);
|
||||
|
||||
if (a.equals(b)) {
|
||||
diff.push({ name, type: CatalogItemDiffType.Unmodified, item: a });
|
||||
} else {
|
||||
diff.push({
|
||||
name,
|
||||
type: CatalogItemDiffType.Modified,
|
||||
a: a,
|
||||
b: b,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
diff.push({
|
||||
name,
|
||||
type: CatalogItemDiffType.Removed,
|
||||
item: metadataA.get(name),
|
||||
});
|
||||
}
|
||||
} else if (name in metadataB.items) {
|
||||
diff.push({
|
||||
name,
|
||||
type: CatalogItemDiffType.Added,
|
||||
item: metadataB.get(name),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return diff;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
import { ffi } from '../../../core/blitzkit/ffi';
|
||||
import InternalBaseLayout from '../../../layouts/InternalBaseLayout.astro';
|
||||
import { Bindings } from '../bindings/_index';
|
||||
import { Discovery, DiscoveryServer } from '../discovery/_index';
|
||||
import { Page } from './_index';
|
||||
|
||||
const serversMap = new Map<string, DiscoveryServer>();
|
||||
|
||||
await Promise.all(
|
||||
[ffi.get_bindings_url_dev()!, ffi.get_bindings_url_production()!]
|
||||
.map((raw) => JSON.parse(raw) as string)
|
||||
.map(async (url) => {
|
||||
const { bindings } = await fetch(url).then(
|
||||
(response) => response.json() as Promise<Bindings>,
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
bindings.map(async (binding) => {
|
||||
const discovery = await fetch(binding.discovery).then(
|
||||
(response) => response.json() as Promise<Discovery>,
|
||||
);
|
||||
|
||||
Object.values(discovery.servers).map((server) => {
|
||||
if (serversMap.has(server.name)) return;
|
||||
serversMap.set(server.name, server);
|
||||
});
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const servers = [...serversMap.values()].sort((a, b) =>
|
||||
a.name.localeCompare(b.name),
|
||||
);
|
||||
---
|
||||
|
||||
<InternalBaseLayout>
|
||||
<Page {servers} client:load />
|
||||
</InternalBaseLayout>
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Flex, Heading, Link, Text } from '@radix-ui/themes';
|
||||
import { PageWrapper } from 'packages/website-ue/src/components/PageWrapper';
|
||||
import { ffi } from 'packages/website-ue/src/core/blitzkit/ffi';
|
||||
import type { Bindings } from '../bindings/_index';
|
||||
|
||||
export interface Discovery {
|
||||
version: string;
|
||||
clusters: string[];
|
||||
default_server: string;
|
||||
servers: Record<string, DiscoveryServer>;
|
||||
icmp_servers: ICMPServers;
|
||||
clusters_versions: Record<string, Empty[]>;
|
||||
countries_by_clusters: CountriesByClusters;
|
||||
extensions: Extensions;
|
||||
}
|
||||
|
||||
interface Empty {
|
||||
version: string;
|
||||
server: string;
|
||||
}
|
||||
|
||||
type CountriesByClusters = Record<string, { countries: string[] }>;
|
||||
|
||||
interface Extensions {
|
||||
endpoints: Record<string, Endpoint>;
|
||||
}
|
||||
|
||||
interface Endpoint {
|
||||
playability_config_url: string;
|
||||
fullscreen_notifications_url: string;
|
||||
mgg_frontier_url: string;
|
||||
mgg_pay_url: string;
|
||||
}
|
||||
|
||||
interface ICMPServers {
|
||||
default: Record<string, string[]>;
|
||||
}
|
||||
|
||||
export interface DiscoveryServer {
|
||||
name: string;
|
||||
addr: string;
|
||||
addr_dsapp?: string;
|
||||
}
|
||||
|
||||
const servers: DiscoveryServer[] = [];
|
||||
|
||||
async function populate(url: string) {
|
||||
const { bindings } = await fetch(url).then(
|
||||
(response) => response.json() as Promise<Bindings>,
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
bindings.map(async (binding) => {
|
||||
const discovery = await fetch(binding.discovery).then(
|
||||
(response) => response.json() as Promise<Discovery>,
|
||||
);
|
||||
|
||||
Object.values(discovery.servers).map((server) => {
|
||||
const doesInclude = servers.some((s) => s.addr === server.addr);
|
||||
if (!doesInclude) servers.push(server);
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
populate(JSON.parse(ffi.get_bindings_url_dev()!)),
|
||||
populate(JSON.parse(ffi.get_bindings_url_production()!)),
|
||||
]);
|
||||
|
||||
servers.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
export function Page() {
|
||||
return (
|
||||
<PageWrapper>
|
||||
{servers.map((server) => (
|
||||
<Flex direction="column" key={server.addr}>
|
||||
<Heading>{server.name}</Heading>
|
||||
|
||||
<Text>
|
||||
Address:{' '}
|
||||
<Link href={server.addr} target="_blank">
|
||||
{server.addr}
|
||||
</Link>
|
||||
</Text>
|
||||
|
||||
{server.addr_dsapp && (
|
||||
<Text>
|
||||
DSApp:{' '}
|
||||
<Link href={server.addr_dsapp} target="_blank">
|
||||
{server.addr_dsapp}
|
||||
</Link>
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import InternalBaseLayout from 'packages/website-ue/src/layouts/InternalBaseLayout.astro';
|
||||
import { Page } from './_index';
|
||||
---
|
||||
|
||||
<InternalBaseLayout>
|
||||
<Page />
|
||||
</InternalBaseLayout>
|
||||
Reference in new issue
Block a user