This commit is contained in:
dengqichen 2024-12-27 16:27:05 +08:00
parent 448350dea2
commit 4b3015f374
21 changed files with 4910 additions and 235 deletions

21
frontend/components.json Normal file
View File

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

File diff suppressed because it is too large Load Diff

View File

@ -26,6 +26,12 @@
"@logicflow/core": "^2.0.9",
"@logicflow/extension": "^2.0.13",
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-avatar": "^1.1.2",
"@radix-ui/react-progress": "^1.1.1",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.2",
"@reduxjs/toolkit": "^2.0.1",
"@types/recharts": "^1.8.29",
"ajv": "^8.17.1",
@ -46,16 +52,24 @@
"devDependencies": {
"@types/dagre": "^0.7.52",
"@types/fs-extra": "^11.0.4",
"@types/node": "^20.10.4",
"@types/node": "^20.17.10",
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"eslint": "^8.55.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"fs-extra": "^11.2.0",
"lucide-react": "^0.469.0",
"postcss": "^8.4.49",
"tailwind-merge": "^2.6.0",
"tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.3.3",
"vite": "^5.0.8"
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg">
<title>Deploy Ease</title>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<circle fill="#1890FF" cx="16" cy="16" r="16"/>
<path d="M16,8 L24,16 L20,16 L20,24 L12,24 L12,16 L8,16 L16,8 Z" fill="#FFFFFF"/>
</g>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<circle cx="16" cy="16" r="14" fill="#2196F3"/>
<path d="M16 6a10 10 0 0 0-10 10 10 10 0 0 0 10 10 10 10 0 0 0 10-10h-2a8 8 0 0 1-8 8 8 8 0 0 1-8-8 8 8 0 0 1 8-8v-2z" fill="#FFFFFF"/>
<circle cx="16" cy="16" r="4" fill="#FFFFFF"/>
<path d="M17.5 14.5l1.5-1.5 1.5 1.5-1.5 1.5z" fill="#2196F3"/>
<path d="M14.5 17.5l-1.5 1.5-1.5-1.5 1.5-1.5z" fill="#2196F3"/>
</svg>

Before

Width:  |  Height:  |  Size: 405 B

After

Width:  |  Height:  |  Size: 507 B

View File

@ -0,0 +1,48 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@ -0,0 +1,26 @@
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }

View File

@ -0,0 +1,157 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@ -0,0 +1,29 @@
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

View File

@ -0,0 +1,53 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@ -1,11 +1,91 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@ -4,6 +4,7 @@ import { RouterProvider } from 'react-router-dom';
import { Provider } from 'react-redux';
import router from './router';
import store from './store';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<Provider store={store}>

View File

@ -1,232 +1,280 @@
import React from 'react';
import { Card, Row, Col, Statistic, Table, Tag, Space, Progress, List, Avatar } from 'antd';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { RocketOutlined, CloudServerOutlined, CheckCircleOutlined, ClockCircleOutlined } from '@ant-design/icons';
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import {
Clock,
Search,
Edit,
ChevronRight,
CheckCircle,
AlertTriangle,
XCircle
} from "lucide-react";
// Mock 数据
const mockDeploymentStats = {
totalDeployments: 1892,
successRate: 98.5,
activeEnvironments: 12,
averageDeployTime: '3.5分钟'
};
const mockDeploymentTrend = [
{ date: '12-11', deployments: 35, success: 33, failed: 2 },
{ date: '12-12', deployments: 42, success: 40, failed: 2 },
{ date: '12-13', deployments: 28, success: 28, failed: 0 },
{ date: '12-14', deployments: 45, success: 43, failed: 2 },
{ date: '12-15', deployments: 50, success: 48, failed: 2 },
{ date: '12-16', deployments: 38, success: 37, failed: 1 },
{ date: '12-17', deployments: 40, success: 39, failed: 1 }
];
const mockEnvironments = [
// Mock环境数据
const environments = [
{
name: '生产环境',
status: 'HEALTHY',
lastDeployment: '2024-12-17 20:30:00',
resourceUsage: 78,
services: 15
id: "dev",
name: "开发环境",
projectCount: 10,
status: "success",
lastDeployment: "10分钟前",
cpu: 60,
memory: 70,
storage: 50
},
{
name: 'UAT环境',
status: 'HEALTHY',
lastDeployment: '2024-12-17 19:45:00',
resourceUsage: 65,
services: 15
id: "test",
name: "测试环境",
projectCount: 8,
status: "warning",
lastDeployment: "1小时前",
cpu: 80,
memory: 75,
storage: 60
},
{
name: '测试环境',
status: 'WARNING',
lastDeployment: '2024-12-17 18:20:00',
resourceUsage: 85,
services: 12
id: "staging",
name: "预发环境",
projectCount: 5,
status: "success",
lastDeployment: "2小时前",
cpu: 40,
memory: 50,
storage: 30
},
{
name: '开发环境',
status: 'HEALTHY',
lastDeployment: '2024-12-17 17:15:00',
resourceUsage: 45,
services: 15
id: "prod",
name: "生产环境",
projectCount: 12,
status: "error",
lastDeployment: "1天前",
cpu: 90,
memory: 85,
storage: 70
}
];
const mockRecentActivities = [
// Mock项目数据
const projects = [
{
id: 1,
type: 'deployment',
project: '用户中心服务',
environment: '生产环境',
status: 'SUCCESS',
operator: '张三',
time: '10分钟前',
version: 'v2.3.0'
name: "用户中心",
code: "user-center",
type: "微服务",
version: "v2.3.1",
status: "活跃",
buildStatus: "success",
lastDeployment: "30分钟前"
},
{
id: 2,
type: 'deployment',
project: '订单服务',
environment: 'UAT环境',
status: 'IN_PROGRESS',
operator: '李四',
time: '25分钟前',
version: 'v1.8.5'
name: "订单系统",
code: "order-system",
type: "后端服务",
version: "v1.7.0",
status: "活跃",
buildStatus: "error",
lastDeployment: "2小时前"
},
{
id: 3,
type: 'deployment',
project: '支付服务',
environment: '测试环境',
status: 'FAILED',
operator: '王五',
time: '1小时前',
version: 'v2.1.0'
name: "前端门户",
code: "frontend-portal",
type: "前端应用",
version: "v3.1.2",
status: "活跃",
buildStatus: "running",
lastDeployment: "1天前"
}
];
const Dashboard: React.FC = () => {
const getStatusTag = (status: string) => {
const statusMap: Record<string, { color: string; text: string }> = {
SUCCESS: { color: 'success', text: '成功' },
FAILED: { color: 'error', text: '失败' },
IN_PROGRESS: { color: 'processing', text: '进行中' },
HEALTHY: { color: 'success', text: '健康' },
WARNING: { color: 'warning', text: '警告' },
ERROR: { color: 'error', text: '错误' }
};
const statusInfo = statusMap[status] || { color: 'default', text: status };
return <Tag color={statusInfo.color}>{statusInfo.text}</Tag>;
const [projectType, setProjectType] = useState("");
const [status, setStatus] = useState("");
const getStatusIcon = (status: string) => {
switch (status) {
case 'success':
return <CheckCircle className="h-5 w-5 text-green-500" />;
case 'warning':
return <AlertTriangle className="h-5 w-5 text-yellow-500" />;
case 'error':
return <XCircle className="h-5 w-5 text-red-500" />;
default:
return <Clock className="h-5 w-5 text-blue-500" />;
}
};
const getResourceUsageStatus = (usage: number) => {
if (usage >= 80) return 'exception';
if (usage >= 70) return 'warning';
return 'success';
const getBuildStatusBadge = (status: string) => {
const statusConfig = {
success: { className: "bg-green-100 text-green-800", text: "成功" },
error: { className: "bg-red-100 text-red-800", text: "失败" },
running: { className: "bg-blue-100 text-blue-800", text: "进行中" }
};
const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.running;
return (
<Badge className={cn("rounded-full", config.className)}>
{config.text}
</Badge>
);
};
return (
<div className="p-6">
{/* 部署统计 */}
<Row gutter={16} className="mb-6">
<Col span={6}>
<Card>
<Statistic
title="总部署次数"
value={mockDeploymentStats.totalDeployments}
prefix={<RocketOutlined />}
valueStyle={{ color: '#1890ff' }}
/>
<div className="flex-1 p-8">
<h2 className="text-2xl font-semibold mb-6"></h2>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{environments.map((env) => (
<Card key={env.id} className="hover:shadow-lg transition-shadow duration-300">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{env.name}</CardTitle>
{getStatusIcon(env.status)}
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{env.projectCount}</div>
<p className="text-xs text-muted-foreground"></p>
<div className="mt-4 space-y-2">
<div className="flex items-center text-sm">
<Clock className="mr-2 h-4 w-4 text-muted-foreground" />
: {env.lastDeployment}
</div>
<div className="space-y-1">
<div className="flex justify-between text-xs">
<span>CPU</span>
<span>{env.cpu}%</span>
</div>
<Progress value={env.cpu} className="h-1" />
</div>
<div className="space-y-1">
<div className="flex justify-between text-xs">
<span></span>
<span>{env.memory}%</span>
</div>
<Progress value={env.memory} className="h-1" />
</div>
<div className="space-y-1">
<div className="flex justify-between text-xs">
<span></span>
<span>{env.storage}%</span>
</div>
<Progress value={env.storage} className="h-1" />
</div>
</div>
</CardContent>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="部署成功率"
value={mockDeploymentStats.successRate}
suffix="%"
precision={1}
valueStyle={{ color: '#52c41a' }}
prefix={<CheckCircleOutlined />}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="活跃环境数"
value={mockDeploymentStats.activeEnvironments}
prefix={<CloudServerOutlined />}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="平均部署时间"
value={mockDeploymentStats.averageDeployTime}
prefix={<ClockCircleOutlined />}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
</Row>
))}
</div>
{/* 部署趋势和环境状态 */}
<Row gutter={16} className="mb-6">
<Col span={16}>
<Card title="部署趋势">
<div style={{ height: 300 }}>
<ResponsiveContainer width="100%" height="100%">
<LineChart data={mockDeploymentTrend}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="deployments" stroke="#1890ff" name="总部署" />
<Line type="monotone" dataKey="success" stroke="#52c41a" name="成功" />
<Line type="monotone" dataKey="failed" stroke="#ff4d4f" name="失败" />
</LineChart>
</ResponsiveContainer>
<div className="bg-white rounded-lg shadow">
<Tabs defaultValue={environments[0].id} className="w-full">
<div className="border-b px-6">
<TabsList className="h-16">
{environments.map((env) => (
<TabsTrigger
key={env.id}
value={env.id}
className="px-6 py-3 data-[state=active]:border-b-2 data-[state=active]:border-blue-600"
>
{env.name}
</TabsTrigger>
))}
</TabsList>
</div>
</Card>
</Col>
<Col span={8}>
<Card title="环境状态">
<List
dataSource={mockEnvironments}
renderItem={item => (
<List.Item>
<div className="w-full">
<div className="flex justify-between items-center mb-2">
<span className="font-medium">{item.name}</span>
{getStatusTag(item.status)}
</div>
<div className="text-sm text-gray-500 mb-2">
: {item.lastDeployment}
</div>
<Progress
percent={item.resourceUsage}
size="small"
status={getResourceUsageStatus(item.resourceUsage)}
format={percent => `资源使用率 ${percent}%`}
/>
</div>
</List.Item>
)}
/>
</Card>
</Col>
</Row>
{/* 最近活动 */}
<Card title="最近部署活动" extra={<a href="#"></a>}>
<List
dataSource={mockRecentActivities}
renderItem={item => (
<List.Item>
<List.Item.Meta
avatar={<Avatar icon={<RocketOutlined />} />}
title={
<Space>
<span>{item.project}</span>
<Tag>{item.environment}</Tag>
{getStatusTag(item.status)}
</Space>
}
description={
<Space>
<span>: {item.version}</span>
<span>: {item.operator}</span>
<span>{item.time}</span>
</Space>
}
/>
</List.Item>
)}
/>
{environments.map((env) => (
<TabsContent key={env.id} value={env.id} className="p-6">
<div className="mb-6">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Input placeholder="项目名称或代码" />
<Select value={projectType} onValueChange={setProjectType}>
<SelectTrigger>
<SelectValue placeholder="项目类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="frontend"></SelectItem>
<SelectItem value="backend"></SelectItem>
<SelectItem value="microservice"></SelectItem>
</SelectContent>
</Select>
<Select value={status} onValueChange={setStatus}>
<SelectTrigger>
<SelectValue placeholder="项目状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="active"></SelectItem>
<SelectItem value="archived"></SelectItem>
<SelectItem value="paused"></SelectItem>
</SelectContent>
</Select>
<div className="flex space-x-2">
<Button className="flex-1">
<Search className="w-4 h-4 mr-2" />
</Button>
<Button variant="outline" className="flex-1"></Button>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{projects.map((project) => (
<Card key={project.id} className="hover:shadow-md transition-shadow duration-300">
<CardContent className="p-4">
<div className="flex justify-between items-start mb-2">
<div>
<h3 className="text-lg font-semibold">{project.name}</h3>
<p className="text-sm text-gray-500">{project.code}</p>
</div>
{getBuildStatusBadge(project.buildStatus)}
</div>
<div className="grid grid-cols-2 gap-2 text-sm mb-4">
<div>
<p className="text-gray-500"></p>
<p className="font-medium">{project.type}</p>
</div>
<div>
<p className="text-gray-500"></p>
<p className="font-medium">{project.version}</p>
</div>
<div>
<p className="text-gray-500"></p>
<p className="font-medium">{project.status}</p>
</div>
<div>
<p className="text-gray-500"></p>
<p className="font-medium">{project.lastDeployment}</p>
</div>
</div>
<div className="flex justify-end space-x-2">
<Button variant="outline" size="sm">
<Edit className="h-4 w-4 mr-1" />
</Button>
<Button variant="outline" size="sm">
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</TabsContent>
))}
</Tabs>
</div>
</div>
);
};

View File

@ -0,0 +1,93 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
prefix: "",
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px'
}
},
extend: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
keyframes: {
'accordion-down': {
from: {
height: '0'
},
to: {
height: 'var(--radix-accordion-content-height)'
}
},
'accordion-up': {
from: {
height: 'var(--radix-accordion-content-height)'
},
to: {
height: '0'
}
}
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out'
}
}
},
plugins: [require("tailwindcss-animate")],
}

View File

@ -1,24 +1,29 @@
{
"compilerOptions": {
"target": "ESNext",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "Node",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"typeRoots": ["./node_modules/@types", "./src/types"],
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Path Aliases */
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
"@/*": ["./src/*"]
}
},
"include": ["src"],