From a54597c65e172e24d2748aec8b05babec8cba576 Mon Sep 17 00:00:00 2001 From: abearxiong Date: Thu, 26 Feb 2026 01:11:45 +0800 Subject: [PATCH] feat: enhance repository management UI and functionality - Added navigation to repository details in RepoCard component. - Implemented a new BuildConfig component for managing build configurations. - Integrated build configuration initialization and saving logic in the store. - Updated RepoInfoCard to include workspace management features. - Improved repository editing dialog with better state handling. - Enhanced repository list fetching with search capabilities. - Added support for creating and managing development configurations. - Refactored code for better readability and maintainability. --- package.json | 6 +- pnpm-lock.yaml | 206 ++++++++++++++++++++ src/pages/repos/components/BuildConfig.tsx | 130 ++++++++++++ src/pages/repos/components/RepoCard.tsx | 54 ++--- src/pages/repos/components/RepoInfoCard.tsx | 186 +++++++++++++++--- src/pages/repos/modules/EditRepoDialog.tsx | 6 +- src/pages/repos/page.tsx | 18 +- src/pages/repos/repo/page.tsx | 33 +++- src/pages/repos/store/build.ts | 54 +++++ src/pages/repos/store/index.ts | 157 ++++++++++++++- 10 files changed, 785 insertions(+), 65 deletions(-) create mode 100644 src/pages/repos/components/BuildConfig.tsx diff --git a/package.json b/package.json index 97f8050..33c39b1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@kevisual/cnb-center", "private": true, - "version": "0.0.5", + "version": "0.0.6", "type": "module", "basename": "/root/cnb-center", "scripts": { @@ -9,7 +9,7 @@ "build": "vite build", "preview": "vite preview", "ui": "pnpm dlx shadcn@latest add ", - "pub": "envision deploy ./dist -k cnb-center -v 0.0.5 -y y -u" + "pub": "envision deploy ./dist -k cnb-center -v 0.0.6 -y y -u" }, "files": [ "dist" @@ -28,6 +28,7 @@ "@kevisual/kv-login": "^0.1.15", "@kevisual/router": "0.0.80", "@tanstack/react-router": "^1.161.1", + "@uiw/react-codemirror": "^4.25.5", "ai": "^6.0.91", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -49,6 +50,7 @@ "access": "public" }, "devDependencies": { + "@codemirror/lang-yaml": "^6.1.2", "@kevisual/gitea": "^0.0.6", "@kevisual/query": "0.0.49", "@kevisual/types": "^0.0.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8fc7f32..ebd3ecf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: '@tanstack/react-router': specifier: ^1.161.1 version: 1.162.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@uiw/react-codemirror': + specifier: ^4.25.5 + version: 4.25.5(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.0)(@codemirror/language@6.12.2)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.39.15)(codemirror@6.0.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ai: specifier: ^6.0.91 version: 6.0.97(zod@4.3.6) @@ -90,6 +93,9 @@ importers: specifier: ^5.0.11 version: 5.0.11(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) devDependencies: + '@codemirror/lang-yaml': + specifier: ^6.1.2 + version: 6.1.2 '@kevisual/gitea': specifier: ^0.0.6 version: 0.0.6 @@ -295,6 +301,33 @@ packages: '@types/react': optional: true + '@codemirror/autocomplete@6.20.0': + resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==} + + '@codemirror/commands@6.10.2': + resolution: {integrity: sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==} + + '@codemirror/lang-yaml@6.1.2': + resolution: {integrity: sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==} + + '@codemirror/language@6.12.2': + resolution: {integrity: sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==} + + '@codemirror/lint@6.9.4': + resolution: {integrity: sha512-ABc9vJ8DEmvOWuH26P3i8FpMWPQkduD9Rvba5iwb6O3hxASgclm3T3krGo8NASXkHCidz6b++LWlzWIUfEPSWw==} + + '@codemirror/search@6.6.0': + resolution: {integrity: sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==} + + '@codemirror/state@6.5.4': + resolution: {integrity: sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==} + + '@codemirror/theme-one-dark@6.1.3': + resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==} + + '@codemirror/view@6.39.15': + resolution: {integrity: sha512-aCWjgweIIXLBHh7bY6cACvXuyrZ0xGafjQ2VInjp4RM4gMfscK5uESiNdrH0pE+e1lZr2B4ONGsjchl2KsKZzg==} + '@emnapi/core@1.8.1': resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} @@ -545,6 +578,21 @@ packages: resolution: {integrity: sha512-jLsL80wBBKkrJZrfk3SQpJ9JA/zREdlUROj7eCkmzqduAWKSI0wVcXuCKf+mLFCHB0Q0Tkh2rgzjSlurt3JQgw==} engines: {node: '>=10.0.0'} + '@lezer/common@1.5.1': + resolution: {integrity: sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/lr@1.4.8': + resolution: {integrity: sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==} + + '@lezer/yaml@1.0.4': + resolution: {integrity: sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==} + + '@marijn/find-cluster-break@1.0.2': + resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + '@napi-rs/wasm-runtime@1.1.1': resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} @@ -856,6 +904,28 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@uiw/codemirror-extensions-basic-setup@4.25.5': + resolution: {integrity: sha512-2KWS4NqrS9SQzlPs/3sxFhuArvjB3JF6WpsrZqBtGHM5/smCNTULX3lUGeRH+f3mkfMt0k6DR+q0xCW9k+Up5w==} + peerDependencies: + '@codemirror/autocomplete': '>=6.0.0' + '@codemirror/commands': '>=6.0.0' + '@codemirror/language': '>=6.0.0' + '@codemirror/lint': '>=6.0.0' + '@codemirror/search': '>=6.0.0' + '@codemirror/state': '>=6.0.0' + '@codemirror/view': '>=6.0.0' + + '@uiw/react-codemirror@4.25.5': + resolution: {integrity: sha512-WUMBGwfstufdbnaiMzQzmOf+6Mzf0IbiOoleexC9ItWcDTJybidLtEi20aP2N58Wn/AQxsd5Otebydaimh7Opw==} + peerDependencies: + '@babel/runtime': '>=7.11.0' + '@codemirror/state': '>=6.0.0' + '@codemirror/theme-one-dark': '>=6.0.0' + '@codemirror/view': '>=6.0.0' + codemirror: '>=6.0.0' + react: '>=17.0.0' + react-dom: '>=17.0.0' + '@vercel/oidc@3.1.0': resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} engines: {node: '>= 20'} @@ -931,6 +1001,9 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + codemirror@6.0.2: + resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -940,6 +1013,9 @@ packages: cookie-es@2.0.0: resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==} + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + crossws@0.3.5: resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} @@ -1333,6 +1409,9 @@ packages: spark-md5@3.0.2: resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==} + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + tabbable@6.4.0: resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} @@ -1505,6 +1584,9 @@ packages: yaml: optional: true + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} @@ -1721,6 +1803,69 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + '@codemirror/autocomplete@6.20.0': + dependencies: + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.15 + '@lezer/common': 1.5.1 + + '@codemirror/commands@6.10.2': + dependencies: + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.15 + '@lezer/common': 1.5.1 + + '@codemirror/lang-yaml@6.1.2': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + '@lezer/yaml': 1.0.4 + + '@codemirror/language@6.12.2': + dependencies: + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.15 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + style-mod: 4.1.3 + + '@codemirror/lint@6.9.4': + dependencies: + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.15 + crelt: 1.0.6 + + '@codemirror/search@6.6.0': + dependencies: + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.15 + crelt: 1.0.6 + + '@codemirror/state@6.5.4': + dependencies: + '@marijn/find-cluster-break': 1.0.2 + + '@codemirror/theme-one-dark@6.1.3': + dependencies: + '@codemirror/language': 6.12.2 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.15 + '@lezer/highlight': 1.2.3 + + '@codemirror/view@6.39.15': + dependencies: + '@codemirror/state': 6.5.4 + crelt: 1.0.6 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + '@emnapi/core@1.8.1': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -2002,6 +2147,24 @@ snapshots: '@kevisual/ws@8.19.0': {} + '@lezer/common@1.5.1': {} + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.1 + + '@lezer/lr@1.4.8': + dependencies: + '@lezer/common': 1.5.1 + + '@lezer/yaml@1.0.4': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@marijn/find-cluster-break@1.0.2': {} + '@napi-rs/wasm-runtime@1.1.1': dependencies: '@emnapi/core': 1.8.1 @@ -2278,6 +2441,33 @@ snapshots: dependencies: csstype: 3.2.3 + '@uiw/codemirror-extensions-basic-setup@4.25.5(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.2)(@codemirror/language@6.12.2)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/view@6.39.15)': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/commands': 6.10.2 + '@codemirror/language': 6.12.2 + '@codemirror/lint': 6.9.4 + '@codemirror/search': 6.6.0 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.15 + + '@uiw/react-codemirror@4.25.5(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.0)(@codemirror/language@6.12.2)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.39.15)(codemirror@6.0.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime': 7.28.6 + '@codemirror/commands': 6.10.2 + '@codemirror/state': 6.5.4 + '@codemirror/theme-one-dark': 6.1.3 + '@codemirror/view': 6.39.15 + '@uiw/codemirror-extensions-basic-setup': 4.25.5(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.2)(@codemirror/language@6.12.2)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/view@6.39.15) + codemirror: 6.0.2 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + transitivePeerDependencies: + - '@codemirror/autocomplete' + - '@codemirror/language' + - '@codemirror/lint' + - '@codemirror/search' + '@vercel/oidc@3.1.0': {} '@vitejs/plugin-react@5.1.4(vite@8.0.0-beta.15(@types/node@25.3.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))': @@ -2364,12 +2554,24 @@ snapshots: clsx@2.1.1: {} + codemirror@6.0.2: + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/commands': 6.10.2 + '@codemirror/language': 6.12.2 + '@codemirror/lint': 6.9.4 + '@codemirror/search': 6.6.0 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.15 + convert-source-map@2.0.0: {} cookie-es@1.2.2: {} cookie-es@2.0.0: {} + crelt@1.0.6: {} + crossws@0.3.5: dependencies: uncrypto: 0.1.3 @@ -2690,6 +2892,8 @@ snapshots: spark-md5@3.0.2: {} + style-mod@4.1.3: {} + tabbable@6.4.0: {} tailwind-merge@3.5.0: {} @@ -2775,6 +2979,8 @@ snapshots: jiti: 2.6.1 tsx: 4.21.0 + w3c-keyname@2.2.8: {} + webpack-virtual-modules@0.6.2: {} yallist@3.1.1: {} diff --git a/src/pages/repos/components/BuildConfig.tsx b/src/pages/repos/components/BuildConfig.tsx new file mode 100644 index 0000000..3ecb64b --- /dev/null +++ b/src/pages/repos/components/BuildConfig.tsx @@ -0,0 +1,130 @@ +import { useEffect, useState } from "react"; +import { useRepoStore } from "../store"; +import { useShallow } from "zustand/shallow"; +import { toast } from "sonner"; +import CodeMirror from "@uiw/react-codemirror"; +import { yaml } from "@codemirror/lang-yaml"; +import { useLayoutStore } from "@/pages/auth/store"; +import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Workflow } from "lucide-react"; + +export const BuildConfig = () => { + const repoStore = useRepoStore(useShallow((state) => ({ + getItem: state.getItem, + editRepo: state.editRepo, + buildConfig: state.buildConfig, + setBuildConfig: state.setBuildConfig, + initBuildConfig: state.initBuildConfig, + deleteBuildConfig: state.deleteBuildConfig, + loading: state.loading, + buildWorkspace: state.buildWorkspace, + }))); + const repo = repoStore.editRepo!; + const me = useLayoutStore((state) => state.me); + const [localConfig, setLocalConfig] = useState(repoStore.buildConfig?.config || ""); + + // 同步 buildConfig 变化时的状态 + useEffect(() => { + setLocalConfig(repoStore.buildConfig?.config || ""); + }, [repoStore.buildConfig]); + useEffect(() => { + if (repo) { + repoStore.initBuildConfig({ repo: repo, user: me }); + } + }, [repo, me]) + + const handleSave = () => { + if (repoStore.buildConfig) { + repoStore.setBuildConfig({ + ...repoStore.buildConfig, + config: localConfig, + }, true); + } + }; + + const handleFieldChange = (field: string, value: string | null) => { + if (repoStore.buildConfig && value !== null) { + repoStore.setBuildConfig({ + ...repoStore.buildConfig, + [field]: value, + }, false); + } + }; + if (repoStore.loading) { + return
Loading...
+ } + return ( +
+ {/* 左侧边栏 - 配置信息 */} +
+
+ 构建配置 + +
+
+ + handleFieldChange("repo", e.target.value)} + placeholder="仓库名称" + /> +
+
+ + handleFieldChange("branch", e.target.value)} + placeholder="分支名称" + /> +
+
+ + handleFieldChange("event", e.target.value)} + placeholder="事件名称" + /> +
+
+ + {/* 右侧 - 编辑器 */} +
+
+ 配置文件 +
+ + +
+
+
+ setLocalConfig(value)} + theme="light" + /> +
+
+
+ ) +} + +export default BuildConfig; diff --git a/src/pages/repos/components/RepoCard.tsx b/src/pages/repos/components/RepoCard.tsx index a0221b0..7535e17 100644 --- a/src/pages/repos/components/RepoCard.tsx +++ b/src/pages/repos/components/RepoCard.tsx @@ -13,13 +13,14 @@ import { PopoverContent, PopoverTrigger, } from '@/components/ui/popover' -import { Star, GitFork, FileText, Edit, FolderGit2, MoreVertical, FileText as IssueIcon, Settings, Play, Trash2, RefreshCw, BookOpen, Copy, Clock, Info, Eye, Square } from 'lucide-react' +import { Star, GitFork, FileText, Edit, FolderGit2, MoreVertical, FileText as IssueIcon, Settings, Play, Trash2, RefreshCw, BookOpen, Copy, Clock, Info, Eye, Square, LinkIcon, ExternalLink } from 'lucide-react' import { useRepoStore } from '../store' import { useMemo, useState } from 'react' import { useShallow } from 'zustand/shallow' import { myOrgs } from '../store/build' import { app, cnb } from '@/agents/app' import { toast } from 'sonner' +import { useNavigate } from '@tanstack/react-router' interface RepoCardProps { repo: any @@ -46,13 +47,13 @@ export function RepoCard({ repo, onStartWorkspace, onEdit, onIssue, onSettings, const isWorkspaceActive = !!workspace const owner = repo.path.split('/')[0] const isMine = myOrgs.includes(owner) - + const navigate = useNavigate(); const isKnowledge = repo?.flags === "KnowledgeBase" const createKnow = async () => { const res = await app.run({ path: 'cnb', key: 'build-knowledge-base', payload: { repo: repo.path } }) if (res.code === 200) { toast.success("知识库创建中") - getList(true) + getList({}, true) } } const onClone = async () => { @@ -72,6 +73,14 @@ export function RepoCard({ repo, onStartWorkspace, onEdit, onIssue, onSettings,
+
{ + navigate({ to: `/repo?repo=${repo.path}` }) + }} + > + {repo.path} +
{isKnowledge && ( @@ -88,14 +97,6 @@ export function RepoCard({ repo, onStartWorkspace, onEdit, onIssue, onSettings, )} - - {repo.path} - {isWorkspaceActive && ( @@ -195,13 +196,19 @@ export function RepoCard({ repo, onStartWorkspace, onEdit, onIssue, onSettings, Clone URL + { + window.open(repo.web_url, '_blank') + }} className="cursor-pointer"> + + 访问仓库 + onIssue(repo)} className="cursor-pointer"> - Issue + 访问问题 onSettings(repo)} className="cursor-pointer"> - 设置 + 访问设置 { @@ -268,22 +275,25 @@ export function RepoCard({ repo, onStartWorkspace, onEdit, onIssue, onSettings,
{repo.site && ( - { + window.open(repo.site, '_blank') + }} > - 🔗 {repo.site} - + +
+ {repo.site} +
+
)} - {repo.description && ( -

+

{repo.description}

)} +
diff --git a/src/pages/repos/components/RepoInfoCard.tsx b/src/pages/repos/components/RepoInfoCard.tsx index 95f415b..dee83a6 100644 --- a/src/pages/repos/components/RepoInfoCard.tsx +++ b/src/pages/repos/components/RepoInfoCard.tsx @@ -1,16 +1,35 @@ +import { useNavigate } from "@tanstack/react-router"; +import { useMemo } from "react"; import { useRepoStore } from "../store"; import { useShallow } from "zustand/shallow"; import { toast } from "sonner"; import { Card } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; -import { Star, GitFork, FileText, ExternalLink, Calendar, User, Copy } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { Star, GitFork, FileText, ExternalLink, Calendar, User, Copy, ArrowLeft, Play, Square, Eye, BookOpen, RefreshCw } from "lucide-react"; +import { myOrgs } from "../store/build"; export const RepoInfoCard = () => { - const repoStore = useRepoStore(useShallow((state) => ({ - getItem: state.getItem, + const navigate = useNavigate(); + const { workspaceList, getWorkspaceDetail, stopWorkspace, editRepo, setSelectedSyncRepo, setSyncDialogOpen } = useRepoStore(useShallow((state) => ({ + workspaceList: state.workspaceList, + getWorkspaceDetail: state.getWorkspaceDetail, + stopWorkspace: state.stopWorkspace, editRepo: state.editRepo, + setSelectedSyncRepo: state.setSelectedSyncRepo, + setSyncDialogOpen: state.setSyncDialogOpen, }))); - const repo = repoStore.editRepo!; + const repo = editRepo!; + + const workspace = useMemo(() => { + return workspaceList.find(ws => ws.slug === repo.path) + }, [workspaceList, repo.path]) + const isWorkspaceActive = !!workspace + const owner = repo.path.split('/')[0] + const isMine = myOrgs.includes(owner) + const isKnowledge = repo?.flags === "KnowledgeBase" + const onClone = () => { const url = `git clone https://cnb.cool/${repo.path}` navigator.clipboard.writeText(url).then(() => { @@ -30,6 +49,13 @@ export const RepoInfoCard = () => { {/* 标题行 */}
+ + {repo.path} @@ -43,37 +69,147 @@ export const RepoInfoCard = () => { {repo.visibility_level === 'Public' ? '公开' : repo.visibility_level === 'Private' ? '私有' : repo.visibility_level} + {isKnowledge && ( + + + + +
+ } + /> + +

知识库

+
+ + + )} + {isWorkspaceActive && ( + + + + + )} +
+
+ {isWorkspaceActive && ( + + + { + stopWorkspace(workspace) + }} + className="h-8 w-8 p-0 border-neutral-200 hover:border-red-600 hover:bg-red-600 hover:text-white transition-all cursor-pointer" + > + + + } + /> + +

停止工作区

+
+
+
+ )} + + + { + if (!isWorkspaceActive) { + // TODO: 启动工作区 + } else { + getWorkspaceDetail(workspace) + } + }} + className="h-8 w-8 p-0 border-neutral-200 hover:border-neutral-900 hover:bg-neutral-900 hover:text-white transition-all cursor-pointer" + > + {isWorkspaceActive ? : } + + } + /> + +

{isWorkspaceActive ? '查看工作区' : '启动工作区'}

+
+
+
+ + + CNB +
- - - 在 CNB 上查看 -
{/* 描述 */} {repo.description && ( -

+

{repo.description}

)} - {/* 主题标签 */} - {repo.topics && ( -
- {repo.topics.split(',').map((topic: string, idx: number) => ( - - {topic.trim()} - - ))} -
- )} + {/* 主题标签和知识库 */} +
+ {/* 主题标签 */} + {repo.topics && ( +
+ {repo.topics.split(',').map((topic: string, idx: number) => ( + + {topic.trim()} + + ))} +
+ )} - {/* 语言和更新时间 */} +
+ + {/* 统计信息 */} +
+ + + {repo.star_count} + + + + {repo.fork_count} + + + + {repo.open_issue_count} + + {isWorkspaceActive && ( + + + 运行中 + + )} + {isMine && ( + { + setSelectedSyncRepo(repo) + setSyncDialogOpen(true) + }} + > + + 同步 + + )} +
+ + {/* 更新信息 */}
{repo.last_update_nickname && ( diff --git a/src/pages/repos/modules/EditRepoDialog.tsx b/src/pages/repos/modules/EditRepoDialog.tsx index a8110bf..9cbd2c4 100644 --- a/src/pages/repos/modules/EditRepoDialog.tsx +++ b/src/pages/repos/modules/EditRepoDialog.tsx @@ -59,7 +59,7 @@ export function EditRepoDialog({ open, onOpenChange, repo }: EditRepoDialogProps const onSubmit = async (data: FormData) => { if (!repo) return - + await updateRepoInfo({ path: repo.path, description: data.description?.trim() || '', @@ -67,8 +67,8 @@ export function EditRepoDialog({ open, onOpenChange, repo }: EditRepoDialogProps topics: tags.join(','), license: data.license?.trim() || '', }) - - await getList(true) + + await getList({}, true) onOpenChange(false) } diff --git a/src/pages/repos/page.tsx b/src/pages/repos/page.tsx index 2f7eb44..727170d 100644 --- a/src/pages/repos/page.tsx +++ b/src/pages/repos/page.tsx @@ -165,6 +165,21 @@ export const App = () => {
+ +
+ ) +} + +export const CommonRepoDialog = () => { + const { editRepo, showEditDialog, setShowEditDialog, showCreateDialog, setShowCreateDialog, } = useRepoStore(useShallow((state) => ({ + editRepo: state.editRepo, + showEditDialog: state.showEditDialog, + setShowEditDialog: state.setShowEditDialog, + showCreateDialog: state.showCreateDialog, + setShowCreateDialog: state.setShowCreateDialog, + }))) + return ( + <> { /> - + ) } - export default App; \ No newline at end of file diff --git a/src/pages/repos/repo/page.tsx b/src/pages/repos/repo/page.tsx index 0c77deb..1c1c9e5 100644 --- a/src/pages/repos/repo/page.tsx +++ b/src/pages/repos/repo/page.tsx @@ -1,18 +1,27 @@ import { useSearch } from "@tanstack/react-router"; import { useRepoStore } from "../store"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useShallow } from "zustand/shallow"; import { RepoInfoCard } from "../components/RepoInfoCard"; +import BuildConfig from "../components/BuildConfig"; +import { CommonRepoDialog } from "../page"; export const App = () => { const params = useSearch({ strict: false }) as { repo?: string }; const repoStore = useRepoStore(useShallow((state) => ({ getItem: state.getItem, editRepo: state.editRepo, + refresH: state.refresh, }))); + const [activeTab, setActiveTab] = useState("build"); + const tabs = [ + { key: "build", label: "构建配置" }, + { key: "info", label: "基本信息" }, + ] useEffect(() => { if (params.repo) { repoStore.getItem(params.repo); + repoStore.refresH({ search: params.repo, showTips: false }); } else { console.log('no repo param') } @@ -21,10 +30,30 @@ export const App = () => { return
Loading...
} return ( -
+
+
+
+ {tabs.map(tab => ( +
setActiveTab(tab.key)} + > + {tab.label} +
+ ))} +
+ {activeTab === 'build' && } + {activeTab === 'info' && ( +
+
{JSON.stringify(repoStore.editRepo, null, 2)}
+
+ )} +
+
) } diff --git a/src/pages/repos/store/build.ts b/src/pages/repos/store/build.ts index b3745ee..c5cd4c9 100644 --- a/src/pages/repos/store/build.ts +++ b/src/pages/repos/store/build.ts @@ -49,4 +49,58 @@ export const createCommitBlankConfig = (params: { repo?: string, event: 'api_tri git commit --allow-empty -m "up: ${now}" git push ` +} + +export const createDevConfig = (params: { repo?: string, event?: string }) => { + const event = params?.event || 'api_trigger_event'; + return `##### 配置开始,保留注释 ##### +.common_env: &common_env + env: + # 使用环境变量管理密钥,推荐使用密钥仓库管理密钥, 详情见 readme.md + # 使用仓库密钥时,注释 + ## 可选 API-Key 配置(按需取消注释) + # MINIMAX_API_KEY: '' # Minimax 模型 + # ZHIPU_API_KEY: '' # 智谱 AI + # BAILIAN_CODE_API_KEY: '' # 阿里云百炼 + # VOLCENGINE_API_KEY: '' # 火山引擎 + # CNB_API_KEY: '' # CNB API + + # 可选应用配置 + # FEISHU_APP_ID: '' # 飞书应用 ID + # FEISHU_APP_SECRET: '' # 飞书应用密钥 + + # CNB_COOKIE: '' # 可选配置,用cnb.cool的cookie + USERNAME: root + ASSISTANT_CONFIG_DIR: /workspace/kevisual # ASSISTANT_CONFIG_DIR 环境变量指定了配置文件所在的目录 + # CNB_KEVISUAL_ORG: kevisual # 私密仓库使用环境配置(默认即可,默认为当前用户组CNB_GROUP_SLUG) + # CNB_KEVISUAL_APP: assistant-app # 可选配置(默认即可) + # CNB_OPENCLAW: openclaw # 仓库名(默认即可) + # CNB_OPENWEBUI: open-webui # 仓库名(默认即可) + +##### 配置结束 ##### + +main: + ${event}: + - docker: + image: docker.cnb.cool/kevisual/dev-env:latest + services: + - vscode + - docker + runner: + cpus: 16 + imports: + - https://cnb.cool/kevisual/env/-/blob/main/.env.development + env: !reference [.common_env, env] + stages: + - name: 环境变量 + script: printenv > ~/.env.development + - name: 启动nginx + script: nginx + - name: 初始化开发机 + script: zsh /workspace/scripts/init.sh + # endStages: + # - name: 结束阶段 + # script: bun /workspace/scripts/end.ts + +` } \ No newline at end of file diff --git a/src/pages/repos/store/index.ts b/src/pages/repos/store/index.ts index 7ca41d6..9f16786 100644 --- a/src/pages/repos/store/index.ts +++ b/src/pages/repos/store/index.ts @@ -3,7 +3,8 @@ import { query } from '@/modules/query'; import { toast } from 'sonner'; import { cnb } from '@/agents/app' import { WorkspaceInfo } from '@kevisual/cnb' -import { createBuildConfig, createCommitBlankConfig } from './build'; +import { createBuildConfig, createCommitBlankConfig, createDevConfig } from './build'; +import { useLayoutStore } from '@/pages/auth/store'; interface DisplayModule { activity: boolean; contributors: boolean; @@ -52,6 +53,12 @@ interface Data { type WorkspaceTabType = 'dev' | 'work' +type BuildConfig = { + repo: string; + branch: string; + event: string; + config: string; +} type State = { formData: Record; setFormData: (data: Record) => void; @@ -68,13 +75,13 @@ type State = { setShowEditDialog: (show: boolean) => void; showCreateDialog: boolean; setShowCreateDialog: (show: boolean) => void; - getList: (silent?: boolean) => Promise; + getList: (params?: { search?: string }, silent?: boolean) => Promise; updateRepoInfo: (data: Partial) => Promise; createRepo: (data: { visibility: any, path: string, description: string, license: string }) => Promise; deleteItem: (repo: string) => Promise; workspaceList: WorkspaceInfo[]; getWorkspaceList: () => Promise; - refresh: (opts?: { message?: string, showTips?: boolean }) => Promise; + refresh: (opts?: { message?: string, showTips?: boolean, search?: string }) => Promise; startWorkspace: (data: Partial, params?: { open?: boolean, branch?: string }) => Promise; stopWorkspace: (workspace?: WorkspaceInfo) => Promise; getWorkspaceDetail: (data: WorkspaceInfo) => Promise; @@ -89,6 +96,11 @@ type State = { buildSync: (data: Partial, params: { toRepo?: string, fromRepo?: string }) => Promise; buildUpdate: (data: Partial, params?: any) => Promise; getItem: (repo: string) => Promise; + buildConfig: BuildConfig | null; + setBuildConfig: (config: BuildConfig | null, save?: boolean) => Promise; + deleteBuildConfig: (params: { repo: Data, user?: any }) => Promise; + initBuildConfig: (params: { repo: Data, user?: any }) => Promise; + buildWorkspace: () => Promise; } export const useRepoStore = create((set, get) => { @@ -114,6 +126,127 @@ export const useRepoStore = create((set, get) => { setSyncDialogOpen: (open) => set({ syncDialogOpen: open }), selectedSyncRepo: null, setSelectedSyncRepo: (repo) => set({ selectedSyncRepo: repo }), + buildConfig: null, + setBuildConfig: async (config, save = true) => { + const me = useLayoutStore.getState().me; + if (config && config!.repo && save) { + let path: string = config.repo || ''; + path = path.replace(/\//g, '__'); + const key = `buildConfig_${path}.json`; + if (config && me) { + const res = await query.post({ + path: 'config', + key: 'update', + data: { + key: key, + data: config, + } + }) + if (res.code === 200) { + toast.success('配置已保存') + } else { + toast.error(res.message || '配置保存失败') + } + } else if (config) { + localStorage.setItem(key, JSON.stringify(config)); + } + } + + set({ buildConfig: config }) + }, + deleteBuildConfig: async (params: { repo: Data, user?: any }) => { + const repo = params.repo; + let path: string = repo.path || ''; + path = path.replace(/\//g, '__'); + const key = `buildConfig_${path}.json`; + if (params?.user) { + const res = await query.post({ + path: 'config', + key: 'delete', + data: { + key: key, + } + }) + if (res.code === 200) { + toast.success('配置已删除') + } else { + toast.error(res.message || '配置删除失败') + } + } else { + localStorage.removeItem(key); + toast.success('配置已删除') + } + }, + initBuildConfig: async (params) => { + const repo = params.repo; + if (!repo) { + toast.error('仓库数据异常'); + return; + } + set({ loading: true }) + try { + console.log('初始化构建配置', params) + let path: string = repo.path || ''; + path = path.replace(/\//g, '__'); + const key = `buildConfig_${path}.json`; + if (params?.user) { + const res = await query.post({ + path: 'config', + key: 'get', + data: { + key: key, + } + }) + if (res.code === 200 && res.data?.data) { + set({ buildConfig: res.data.data }) + return; + } + } else { + const localConfig = localStorage.getItem(key); + if (localConfig) { + try { + const config = JSON.parse(localConfig); + set({ buildConfig: config }) + return; + } catch (e) { + console.error('本地配置解析失败', e); + } + } + } + const config: BuildConfig = { + repo: repo.path, + branch: 'main', + event: 'api_trigger_event', + config: createDevConfig({ repo: repo.path, event: 'api_trigger_event' }), + } + set({ buildConfig: config }) + } catch (e) { + toast.error('配置加载失败'); + console.error('配置加载失败', e); + } + finally { + set({ loading: false }) + } + + }, + buildWorkspace: async () => { + const config = get().buildConfig; + if (!config) { + toast.error('请先保存构建配置'); + return; + } + const res = await cnb.build.startBuild(config.repo, { + branch: config.branch, + env: {}, + event: config.event, + config: config.config, + }) + if (res.code === 200) { + toast.success('构建已触发') + } else { + toast.error(res.message || '构建触发失败') + } + }, getItem: async (repo: string) => { const { setLoading } = get(); setLoading(true); @@ -129,13 +262,19 @@ export const useRepoStore = create((set, get) => { setLoading(false); } }, - getList: async (silent = false) => { + getList: async (params?: { search?: string }, silent = false) => { const { setLoading } = get(); if (!silent) { setLoading(true); } try { - const res = await cnb.repo.getRepoList({}) + let opts = {} + if (params?.search) { + opts = { + search: params.search + } + } + const res = await cnb.repo.getRepoList(opts) if (res.code === 200) { const list = res.data! || [] set({ list }); @@ -171,8 +310,8 @@ export const useRepoStore = create((set, get) => { toast.error(res.message || '更新失败'); } }, - refresh: async (opts?: { message?: string, showTips?: boolean }) => { - const getList = get().getList(); + refresh: async (opts?: { message?: string, showTips?: boolean, search?: string }) => { + const getList = get().getList({ search: opts?.search }, true); const getWorkspaceList = get().getWorkspaceList(); await Promise.all([getList, getWorkspaceList]); if (opts?.showTips !== false) { @@ -207,7 +346,7 @@ export const useRepoStore = create((set, get) => { if (res.code === 200) { toast.success('删除成功'); // 刷新列表 - await get().getList(true); + await get().getList({}, true); } else { toast.error(res.message || '删除失败'); } @@ -216,7 +355,7 @@ export const useRepoStore = create((set, get) => { if (e.message?.includes('JSON') || e.message?.includes('json')) { toast.success('删除成功'); // 刷新列表 - await get().getList(true); + await get().getList({}, true); } else { toast.error('删除失败'); console.error('删除错误:', e);