feat: add telemetry route

This commit is contained in:
moonrailgun 2024-03-25 21:53:43 +08:00
parent 3c60261f37
commit f27f3f2f11
14 changed files with 695 additions and 8 deletions

View File

@ -87,9 +87,15 @@ importers:
'@monaco-editor/react': '@monaco-editor/react':
specifier: ^4.6.0 specifier: ^4.6.0
version: 4.6.0(monaco-editor@0.46.0)(react-dom@18.2.0)(react@18.2.0) version: 4.6.0(monaco-editor@0.46.0)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-alert-dialog':
specifier: ^1.0.5
version: 1.0.5(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-avatar': '@radix-ui/react-avatar':
specifier: ^1.0.4 specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) version: 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-collapsible':
specifier: ^1.0.3
version: 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-dialog': '@radix-ui/react-dialog':
specifier: ^1.0.5 specifier: ^1.0.5
version: 1.0.5(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) version: 1.0.5(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)
@ -7165,6 +7171,32 @@ packages:
'@babel/runtime': 7.24.0 '@babel/runtime': 7.24.0
dev: false dev: false
/@radix-ui/react-alert-dialog@1.0.5(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-OrVIOcZL0tl6xibeuGt5/+UxoT2N27KCFOPjFyfXMnchxSHZ/OW7cCX2nGlIYJrbHK/fczPcFzAwvNBB6XBNMA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.24.0
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.21)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.21)(react@18.2.0)
'@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.21)(react@18.2.0)
'@types/react': 18.2.21
'@types/react-dom': 18.2.7
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0): /@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==} resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==}
peerDependencies: peerDependencies:
@ -7210,6 +7242,34 @@ packages:
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
dev: false dev: false
/@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.24.0
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.21)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.21)(react@18.2.0)
'@radix-ui/react-id': 1.0.1(@types/react@18.2.21)(react@18.2.0)
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.21)(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.21)(react@18.2.0)
'@types/react': 18.2.21
'@types/react-dom': 18.2.7
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0): /@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==} resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==}
peerDependencies: peerDependencies:

View File

@ -1,7 +1,9 @@
import React from 'react'; import React from 'react';
import { TipIcon } from './TipIcon';
interface CommonHeaderProps { interface CommonHeaderProps {
title: string; title: string;
desc?: React.ReactNode;
actions?: React.ReactNode; actions?: React.ReactNode;
} }
export const CommonHeader: React.FC<CommonHeaderProps> = React.memo((props) => { export const CommonHeader: React.FC<CommonHeaderProps> = React.memo((props) => {
@ -9,6 +11,8 @@ export const CommonHeader: React.FC<CommonHeaderProps> = React.memo((props) => {
<> <>
<h1 className="text-xl font-bold">{props.title}</h1> <h1 className="text-xl font-bold">{props.title}</h1>
{props.desc && <TipIcon className="ml-1" content={props.desc} />}
{props.actions && <div className="ml-auto">{props.actions}</div>} {props.actions && <div className="ml-auto">{props.actions}</div>}
</> </>
); );

View File

@ -11,7 +11,7 @@ export interface CommonListItem {
id: string; id: string;
title: string; title: string;
content?: React.ReactNode; content?: React.ReactNode;
tags: string[]; tags?: string[];
href: string; href: string;
} }
@ -91,7 +91,7 @@ export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
<div className="text-muted-foreground line-clamp-2 text-xs"> <div className="text-muted-foreground line-clamp-2 text-xs">
{item.content} {item.content}
</div> </div>
{item.tags.length > 0 ? ( {Array.isArray(item.tags) && item.tags.length > 0 ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{item.tags.map((tag) => ( {item.tags.map((tag) => (
<Badge key={tag} variant={getBadgeVariantFromLabel(tag)}> <Badge key={tag} variant={getBadgeVariantFromLabel(tag)}>

View File

@ -8,9 +8,11 @@ export const CommonWrapper: React.FC<CommonWrapperProps> = React.memo(
(props) => { (props) => {
return ( return (
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
<div className="flex h-[52px] items-center px-4 py-2"> {props.header && (
{props.header} <div className="flex h-[52px] items-center px-4 py-2">
</div> {props.header}
</div>
)}
<Separator /> <Separator />

View File

@ -0,0 +1,22 @@
import React from 'react';
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
import { LuHelpCircle } from 'react-icons/lu';
interface TipIconProps {
className?: string;
content: React.ReactNode;
}
export const TipIcon: React.FC<TipIconProps> = React.memo((props) => {
const { className, content } = props;
return (
<Tooltip>
<TooltipTrigger>
<LuHelpCircle className={className} />
</TooltipTrigger>
<TooltipContent className="max-w-xl">{content}</TooltipContent>
</Tooltip>
);
});
TipIcon.displayName = 'TipIcon';

View File

@ -0,0 +1,139 @@
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/utils/style"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@ -0,0 +1,42 @@
import { cn } from '@/utils/style';
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
import React from 'react';
import { LuChevronRight } from 'react-icons/lu';
const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = React.forwardRef<
React.ElementRef<typeof CollapsiblePrimitive.CollapsibleTrigger>,
React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.CollapsibleTrigger>
>(({ asChild, className, ...props }, ref) => {
let children = props.children;
if (typeof props.children === 'string') {
children = (
<div className="flex items-center">
<LuChevronRight
className={cn(
'mr-1 transition-transform group-data-[state=open]/collapsible:rotate-90'
)}
/>
<span className="font-semibold">{props.children}</span>
</div>
);
}
return (
<CollapsiblePrimitive.CollapsibleTrigger
ref={ref}
{...props}
className={cn('group/collapsible', className)}
>
{children}
</CollapsiblePrimitive.CollapsibleTrigger>
);
});
CollapsibleTrigger.displayName =
CollapsiblePrimitive.CollapsibleTrigger.displayName;
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
export { Collapsible, CollapsibleTrigger, CollapsibleContent };

View File

@ -7,6 +7,7 @@
"scripts": { "scripts": {
"dev": "vite --port 10000", "dev": "vite --port 10000",
"build": "vite build", "build": "vite build",
"ui:add": "shadcn-ui add",
"translation:extract": "i18next-toolkit extract", "translation:extract": "i18next-toolkit extract",
"translation:scan": "i18next-toolkit scan", "translation:scan": "i18next-toolkit scan",
"translation:translate": "i18next-toolkit translate", "translation:translate": "i18next-toolkit translate",
@ -23,7 +24,9 @@
"@hookform/resolvers": "^3.3.4", "@hookform/resolvers": "^3.3.4",
"@loadable/component": "^5.16.3", "@loadable/component": "^5.16.3",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",

View File

@ -12,6 +12,7 @@
import { Route as rootRoute } from './routes/__root' import { Route as rootRoute } from './routes/__root'
import { Route as WebsiteImport } from './routes/website' import { Route as WebsiteImport } from './routes/website'
import { Route as TelemetryImport } from './routes/telemetry'
import { Route as RegisterImport } from './routes/register' import { Route as RegisterImport } from './routes/register'
import { Route as MonitorImport } from './routes/monitor' import { Route as MonitorImport } from './routes/monitor'
import { Route as LoginImport } from './routes/login' import { Route as LoginImport } from './routes/login'
@ -19,6 +20,8 @@ import { Route as DashboardImport } from './routes/dashboard'
import { Route as IndexImport } from './routes/index' import { Route as IndexImport } from './routes/index'
import { Route as WebsiteAddImport } from './routes/website/add' import { Route as WebsiteAddImport } from './routes/website/add'
import { Route as WebsiteWebsiteIdImport } from './routes/website/$websiteId' import { Route as WebsiteWebsiteIdImport } from './routes/website/$websiteId'
import { Route as TelemetryAddImport } from './routes/telemetry/add'
import { Route as TelemetryTelemetryIdImport } from './routes/telemetry/$telemetryId'
import { Route as MonitorAddImport } from './routes/monitor/add' import { Route as MonitorAddImport } from './routes/monitor/add'
import { Route as MonitorMonitorIdImport } from './routes/monitor/$monitorId' import { Route as MonitorMonitorIdImport } from './routes/monitor/$monitorId'
@ -29,6 +32,11 @@ const WebsiteRoute = WebsiteImport.update({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any) } as any)
const TelemetryRoute = TelemetryImport.update({
path: '/telemetry',
getParentRoute: () => rootRoute,
} as any)
const RegisterRoute = RegisterImport.update({ const RegisterRoute = RegisterImport.update({
path: '/register', path: '/register',
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
@ -64,6 +72,16 @@ const WebsiteWebsiteIdRoute = WebsiteWebsiteIdImport.update({
getParentRoute: () => WebsiteRoute, getParentRoute: () => WebsiteRoute,
} as any) } as any)
const TelemetryAddRoute = TelemetryAddImport.update({
path: '/add',
getParentRoute: () => TelemetryRoute,
} as any)
const TelemetryTelemetryIdRoute = TelemetryTelemetryIdImport.update({
path: '/$telemetryId',
getParentRoute: () => TelemetryRoute,
} as any)
const MonitorAddRoute = MonitorAddImport.update({ const MonitorAddRoute = MonitorAddImport.update({
path: '/add', path: '/add',
getParentRoute: () => MonitorRoute, getParentRoute: () => MonitorRoute,
@ -98,6 +116,10 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof RegisterImport preLoaderRoute: typeof RegisterImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/telemetry': {
preLoaderRoute: typeof TelemetryImport
parentRoute: typeof rootRoute
}
'/website': { '/website': {
preLoaderRoute: typeof WebsiteImport preLoaderRoute: typeof WebsiteImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
@ -110,6 +132,14 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof MonitorAddImport preLoaderRoute: typeof MonitorAddImport
parentRoute: typeof MonitorImport parentRoute: typeof MonitorImport
} }
'/telemetry/$telemetryId': {
preLoaderRoute: typeof TelemetryTelemetryIdImport
parentRoute: typeof TelemetryImport
}
'/telemetry/add': {
preLoaderRoute: typeof TelemetryAddImport
parentRoute: typeof TelemetryImport
}
'/website/$websiteId': { '/website/$websiteId': {
preLoaderRoute: typeof WebsiteWebsiteIdImport preLoaderRoute: typeof WebsiteWebsiteIdImport
parentRoute: typeof WebsiteImport parentRoute: typeof WebsiteImport
@ -129,6 +159,7 @@ export const routeTree = rootRoute.addChildren([
LoginRoute, LoginRoute,
MonitorRoute.addChildren([MonitorMonitorIdRoute, MonitorAddRoute]), MonitorRoute.addChildren([MonitorMonitorIdRoute, MonitorAddRoute]),
RegisterRoute, RegisterRoute,
TelemetryRoute.addChildren([TelemetryTelemetryIdRoute, TelemetryAddRoute]),
WebsiteRoute.addChildren([WebsiteWebsiteIdRoute, WebsiteAddRoute]), WebsiteRoute.addChildren([WebsiteWebsiteIdRoute, WebsiteAddRoute]),
]) ])

View File

@ -0,0 +1,115 @@
import { trpc } from '@/api/trpc';
import { CommonHeader } from '@/components/CommonHeader';
import { CommonList } from '@/components/CommonList';
import { CommonWrapper } from '@/components/CommonWrapper';
import { Button } from '@/components/ui/button';
import { useDataReady } from '@/hooks/useDataReady';
import { useEvent } from '@/hooks/useEvent';
import { LayoutV2 } from '@/pages/LayoutV2';
import { useCurrentWorkspaceId } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route';
import { Trans, useTranslation } from '@i18next-toolkit/react';
import {
createFileRoute,
useNavigate,
useRouterState,
} from '@tanstack/react-router';
import { LuPlus } from 'react-icons/lu';
export const Route = createFileRoute('/telemetry')({
beforeLoad: routeAuthBeforeLoad,
component: TelemetryComponent,
});
function TelemetryComponent() {
const workspaceId = useCurrentWorkspaceId();
const { t } = useTranslation();
const { data = [] } = trpc.telemetry.all.useQuery({
workspaceId,
});
const navigate = useNavigate();
const pathname = useRouterState({
select: (state) => state.location.pathname,
});
const items = data.map((item) => ({
id: item.id,
title: item.name,
href: `/telemetry/${item.id}`,
}));
useDataReady(
() => data.length > 0,
() => {
if (pathname === Route.fullPath) {
navigate({
to: '/telemetry/$telemetryId',
params: {
telemetryId: data[0].id,
},
});
}
}
);
const handleClickAdd = useEvent(() => {
navigate({
to: '/telemetry/add',
});
});
return (
<LayoutV2
list={
<CommonWrapper
header={
<CommonHeader
title={t('Telemetry')}
desc={
<div className="space-y-2">
<p>
<Trans>
Telemetry is a technology that reports access data even on
pages that are not under your control. As long as the
other website allows the insertion of third-party images
(e.g., forums, blogs, and various rich-text editors), then
the data can be collected and used to analyze the images
when they are loaded by the user.
</Trans>
</p>
<p>
<Trans>
Generally, we will use a one-pixel blank image so that it
will not affect the user's normal use.
</Trans>
</p>
<p>
<Trans>
At the same time, we can also use it in some client-side
application scenarios, such as collecting the frequency of
cli usage, such as collecting the installation of
selfhosted apps, and so on.
</Trans>
</p>
</div>
}
actions={
<Button
variant="outline"
Icon={LuPlus}
onClick={handleClickAdd}
>
{t('Add')}
</Button>
}
/>
}
>
<CommonList hasSearch={true} items={items} />
</CommonWrapper>
}
/>
);
}

View File

@ -0,0 +1,170 @@
import { trpc } from '@/api/trpc';
import { CommonHeader } from '@/components/CommonHeader';
import { CommonWrapper } from '@/components/CommonWrapper';
import { TelemetryMetricsTable } from '@/components/telemetry/TelemetryMetricsTable';
import { TelemetryOverview } from '@/components/telemetry/TelemetryOverview';
import {
AlertDialog,
AlertDialogAction,
AlertDialogContent,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import { Button } from '@/components/ui/button';
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';
import { useGlobalRangeDate } from '@/hooks/useGlobalRangeDate';
import { useCurrentWorkspaceId } from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route';
import { Trans, useTranslation } from '@i18next-toolkit/react';
import { createFileRoute } from '@tanstack/react-router';
import { Card, Typography } from 'antd';
import { LuChevronRight, LuCode2 } from 'react-icons/lu';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible';
export const Route = createFileRoute('/telemetry/$telemetryId')({
beforeLoad: routeAuthBeforeLoad,
component: TelemetryDetailComponent,
});
function TelemetryDetailComponent() {
const { telemetryId } = Route.useParams<{ telemetryId: string }>();
const workspaceId = useCurrentWorkspaceId();
const { t } = useTranslation();
const { startDate, endDate } = useGlobalRangeDate();
const { data: info } = trpc.telemetry.info.useQuery({
workspaceId,
telemetryId,
});
const startAt = startDate.valueOf();
const endAt = endDate.valueOf();
const blankGif = `${window.location.origin}/telemetry/${workspaceId}/${telemetryId}.gif`;
const countBadgeUrl = `${window.location.origin}/telemetry/${workspaceId}/${telemetryId}/badge.svg`;
return (
<CommonWrapper
header={
<CommonHeader
title={info?.name ?? ''}
actions={
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" Icon={LuCode2}>
{t('Usage')}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Usage</AlertDialogTitle>
</AlertDialogHeader>
<div className="space-y-2">
<p>Here is some way to use telemetry:</p>
<Typography.Title level={3}>
Insert to article:
</Typography.Title>
<p>
if your article support raw html, you can direct insert it{' '}
<Typography.Text code={true} copyable={{ text: blankGif }}>
{blankGif}
</Typography.Text>
</p>
<Collapsible>
<CollapsibleTrigger>Advanced</CollapsibleTrigger>
<CollapsibleContent>
<div className="pl-5">
<p>
Some website will not allow send `referer` field. so
its maybe can not track source. so you can mark it by
yourself. for example:
</p>
<p>
<Typography.Text code={true}>
{blankGif}?url=https://xxxxxxxx
</Typography.Text>
</p>
</div>
</CollapsibleContent>
</Collapsible>
<Typography.Title level={3}>
Count your website visitor:
</Typography.Title>
<p>
if your article support raw html, you can direct insert it{' '}
<Typography.Text
code={true}
copyable={{ text: countBadgeUrl }}
>
{countBadgeUrl}
</Typography.Text>
</p>
<p>
Like this: <img src={countBadgeUrl} />
</p>
</div>
<AlertDialogFooter>
<AlertDialogAction>{t('Get!')}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
}
/>
}
>
<ScrollArea className="h-full overflow-hidden p-4">
<ScrollBar orientation="horizontal" />
<Card>
<Card.Grid hoverable={false} className="!w-full">
<TelemetryOverview
telemetryId={telemetryId}
showDateFilter={true}
workspaceId={workspaceId}
/>
</Card.Grid>
<Card.Grid hoverable={false} className="min-h-[470px] !w-1/3">
<TelemetryMetricsTable
telemetryId={telemetryId}
type="source"
title={[t('Source'), t('Views')]}
startAt={startAt}
endAt={endAt}
/>
</Card.Grid>
<Card.Grid hoverable={false} className="min-h-[470px] !w-1/3">
<TelemetryMetricsTable
telemetryId={telemetryId}
type="event"
title={[t('Events'), t('Views')]}
startAt={startAt}
endAt={endAt}
/>
</Card.Grid>
<Card.Grid hoverable={false} className="min-h-[470px] !w-1/3">
<TelemetryMetricsTable
telemetryId={telemetryId}
type="country"
title={[t('Countries'), t('Visitors')]}
startAt={startAt}
endAt={endAt}
/>
</Card.Grid>
</Card>
</ScrollArea>
</CommonWrapper>
);
}

View File

@ -0,0 +1,101 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router';
import { useTranslation } from '@i18next-toolkit/react';
import { Button } from '@/components/ui/button';
import { useEvent } from '@/hooks/useEvent';
import { useCurrentWorkspaceId } from '@/store/user';
import { trpc } from '@/api/trpc';
import { Card, CardContent, CardFooter } from '@/components/ui/card';
import { CommonWrapper } from '@/components/CommonWrapper';
import { routeAuthBeforeLoad } from '@/utils/route';
import { z } from 'zod';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
export const Route = createFileRoute('/telemetry/add')({
beforeLoad: routeAuthBeforeLoad,
component: TelemetryAddComponent,
});
const addFormSchema = z.object({
name: z.string(),
});
function TelemetryAddComponent() {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const addTelemetryMutation = trpc.telemetry.upsert.useMutation();
const utils = trpc.useUtils();
const navigate = useNavigate();
const form = useForm<z.infer<typeof addFormSchema>>({
resolver: zodResolver(addFormSchema),
defaultValues: {
name: '',
},
});
const onSubmit = useEvent(async (values: z.infer<typeof addFormSchema>) => {
const res = await addTelemetryMutation.mutateAsync({
workspaceId,
name: values.name,
});
utils.telemetry.all.refetch();
form.reset();
navigate({
to: '/telemetry/$telemetryId',
params: {
telemetryId: res.id,
},
});
});
return (
<CommonWrapper
header={<h1 className="text-xl font-bold">{t('Add Website')}</h1>}
>
<div className="p-4">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<Card>
<CardContent className="pt-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t('Telemetry Name')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t('Telemetry Name to Display')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
<CardFooter>
<Button type="submit" loading={addTelemetryMutation.isLoading}>
{t('Create')}
</Button>
</CardFooter>
</Card>
</form>
</Form>
</div>
</CommonWrapper>
);
}

View File

@ -36,7 +36,6 @@ function WebsiteComponent() {
id: item.id, id: item.id,
title: item.name, title: item.name,
content: item.domain, content: item.domain,
tags: [],
href: `/website/${item.id}`, href: `/website/${item.id}`,
})); }));

View File

@ -1,10 +1,9 @@
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
import { createFileRoute, useNavigate } from '@tanstack/react-router'; import { createFileRoute, useNavigate } from '@tanstack/react-router';
import { useState } from 'react';
import { z } from 'zod'; import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { t, useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
Form, Form,