feat: add backend

This commit is contained in:
2025-08-28 23:25:59 +08:00
parent 2edf8753a7
commit 47f36cd625
29 changed files with 1489 additions and 253 deletions

5
apps/server/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules
# Keep environment variables out of version control
.env
/generated/prisma

10
apps/server/app.ts Normal file
View File

@@ -0,0 +1,10 @@
import Koa from 'koa';
import { registerMiddlewares } from './middlewares/index.ts';
const app = new Koa();
registerMiddlewares(app);
app.listen(3000, () => {
console.log('server started at http://localhost:3000');
});

View File

@@ -0,0 +1,11 @@
import type { Context } from 'koa';
import prisma from '../libs/db.ts';
export async function list(ctx: Context) {
const list = await prisma.application.findMany({
where: {
valid: 1,
},
});
ctx.body = list;
}

7
apps/server/libs/db.ts Normal file
View File

@@ -0,0 +1,7 @@
import { PrismaClient } from '../generated/prisma/index.js'
const prismaClientSingleton = () => {
return new PrismaClient();
};
export default prismaClientSingleton();

View File

@@ -0,0 +1,10 @@
import { Router } from './router.ts';
import { ResponseTime } from './responseTime.ts';
import type Koa from 'koa';
export function registerMiddlewares(app: Koa) {
const router = new Router();
const responseTime = new ResponseTime();
responseTime.apply(app);
router.apply(app);
}

View File

@@ -0,0 +1,13 @@
import type { Middleware } from './types.ts';
import type Koa from 'koa';
export class ResponseTime implements Middleware {
apply(app: Koa): void {
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
}
}

View File

@@ -0,0 +1,19 @@
import KoaRouter from '@koa/router';
import type Koa from 'koa';
import type { Middleware } from './types.ts';
import * as application from '../controllers/application.ts';
export class Router implements Middleware {
private router: KoaRouter;
constructor() {
this.router = new KoaRouter({
prefix: '/api',
});
this.router.get('/application/list', application.list);
}
apply(app: Koa) {
app.use(this.router.routes());
app.use(this.router.allowedMethods());
}
}

View File

@@ -0,0 +1,5 @@
import type Koa from 'koa';
export abstract class Middleware {
abstract apply(app: Koa, options?: unknown): void;
}

27
apps/server/package.json Normal file
View File

@@ -0,0 +1,27 @@
{
"name": "server",
"version": "1.0.0",
"description": "",
"scripts": {
"dev": "tsx watch ./app.ts"
},
"keywords": [],
"type": "module",
"author": "",
"license": "ISC",
"dependencies": {
"@koa/router": "^14.0.0",
"@prisma/client": "^6.15.0",
"koa": "^3.0.1"
},
"devDependencies": {
"@tsconfig/node-ts": "^23.6.1",
"@tsconfig/node22": "^22.0.2",
"@types/koa": "^3.0.0",
"@types/koa__router": "^12.0.4",
"@types/node": "^24.3.0",
"prisma": "^6.15.0",
"tsx": "^4.20.5",
"typescript": "^5.9.2"
}
}

Binary file not shown.

View File

@@ -0,0 +1,35 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
output = "../generated/prisma"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model Application {
id Int @id @default(autoincrement())
name String
description String?
repository String
valid Int @default(1)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy String
updatedBy String
}
model Environment {
id Int @id @default(autoincrement())
name String
description String?
valid Int @default(1)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy String
updatedBy String
}

View File

@@ -0,0 +1,6 @@
{
"extends": [
"@tsconfig/node22/tsconfig.json",
"@tsconfig/node-ts/tsconfig.json"
]
}

36
apps/web/README.md Normal file
View File

@@ -0,0 +1,36 @@
# Rsbuild project
## Setup
Install the dependencies:
```bash
pnpm install
```
## Get started
Start the dev server, and the app will be available at [http://localhost:3000](http://localhost:3000).
```bash
pnpm dev
```
Build the app for production:
```bash
pnpm build
```
Preview the production build locally:
```bash
pnpm preview
```
## Learn more
To learn more about Rsbuild, check out the following resources:
- [Rsbuild documentation](https://rsbuild.rs) - explore Rsbuild features and APIs.
- [Rsbuild GitHub repository](https://github.com/web-infra-dev/rsbuild) - your feedback and contributions are welcome!

32
apps/web/package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "web",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "rsbuild build",
"check": "biome check --write",
"dev": "rsbuild dev --open",
"format": "biome format --write",
"preview": "rsbuild preview"
},
"dependencies": {
"@arco-design/web-react": "^2.66.4",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router": "^7.8.0"
},
"devDependencies": {
"@arco-plugins/unplugin-react": "2.0.0-beta.5",
"@rsbuild/core": "^1.4.13",
"@rsbuild/plugin-less": "^1.4.0",
"@rsbuild/plugin-react": "^1.3.4",
"@rsbuild/plugin-svgr": "^1.2.2",
"@tailwindcss/postcss": "^4.1.11",
"@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7",
"tailwindcss": "^4.1.11",
"typescript": "^5.9.2"
},
"packageManager": "pnpm@9.15.2+sha512.93e57b0126f0df74ce6bff29680394c0ba54ec47246b9cf321f0121d8d9bb03f750a705f24edc3c1180853afd7c2c3b94196d0a3d53d3e069d9e2793ef11f321"
}

View File

@@ -0,0 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
},
};

View File

@@ -0,0 +1,18 @@
import { ArcoDesignPlugin } from "@arco-plugins/unplugin-react";
import { defineConfig } from "@rsbuild/core";
import { pluginReact } from "@rsbuild/plugin-react";
import { pluginLess } from "@rsbuild/plugin-less";
import { pluginSvgr } from '@rsbuild/plugin-svgr';
export default defineConfig({
plugins: [pluginReact(), pluginLess(), pluginSvgr()],
tools: {
rspack: {
plugins: [
new ArcoDesignPlugin({
defaultLanguage: "zh-CN",
}),
],
},
},
});

View File

@@ -0,0 +1,9 @@
<svg width="33" height="34" viewBox="0 0 33 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.8125 17.2932C32.8125 19.6071 32.3208 21.8488 31.4779 23.8735L25.6476 17.944C26.35 16.787 26.7012 15.4131 26.7012 13.9669C26.7715 9.84522 23.47 6.44662 19.3958 6.44662C17.9207 6.44662 16.586 6.88048 15.4621 7.60359L10.0533 2.10799C12.0904 1.16795 14.2679 0.661774 16.6562 0.661774C25.5773 0.661774 32.8125 8.10976 32.8125 17.2932ZM7.80543 3.40958L13.6357 9.41135C12.6523 10.7129 12.0904 12.3038 12.0904 14.0392C12.0904 18.2332 15.3918 21.6318 19.466 21.6318C21.1519 21.6318 22.6973 21.0534 23.9617 20.041L30.2134 26.4767C27.2632 30.9599 22.3461 33.9246 16.6562 33.9246C7.73519 33.9246 0.5 26.4767 0.5 17.2932C0.5 11.436 3.38003 6.37431 7.80543 3.40958ZM19.466 18.7394C22.2056 18.7394 24.3832 16.4978 24.3832 13.6777C24.3832 10.8576 22.2056 8.61594 19.466 8.61594C16.7265 8.61594 14.5489 10.8576 14.5489 13.6777C14.5489 16.4978 16.7265 18.7394 19.466 18.7394Z" fill="url(#paint0_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="6.88952" y1="1.73016" x2="27.8689" y2="33.2773" gradientUnits="userSpaceOnUse">
<stop stop-color="#73DFE7"/>
<stop offset="1" stop-color="#0095F7"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

11
apps/web/src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
/// <reference types="@rsbuild/core/types" />
/**
* Imports the SVG file as a React component.
* @requires [@rsbuild/plugin-svgr](https://npmjs.com/package/@rsbuild/plugin-svgr)
*/
declare module '*.svg?react' {
import type React from 'react';
const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
export default ReactComponent;
}

17
apps/web/src/index.tsx Normal file
View File

@@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from '@pages/App';
import { BrowserRouter } from 'react-router';
const rootEl = document.getElementById('root');
if (rootEl) {
const root = ReactDOM.createRoot(rootEl);
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
);
}

View File

@@ -0,0 +1,18 @@
import { Route, Routes } from 'react-router';
import Home from '@pages/home';
import Login from '@pages/login';
import Application from '@pages/application';
import '@styles/index.css';
const App = () => {
return (
<Routes>
<Route path="/" element={<Home />}>
<Route path="application" element={<Application />} index />
</Route>
<Route path="/login" element={<Login />} />
</Routes>
);
};
export default App;

View File

@@ -0,0 +1,12 @@
import { useState } from "react";
function Application() {
const [apps, setApps] = useState<Application[]>([]);
return (
<div>
application
</div>
)
}
export default Application;

View File

@@ -0,0 +1,15 @@
enum AppStatus {
Idle = "Pending",
Running = "Running",
Stopped = "Stopped",
}
export interface Application {
id: string;
name: string;
description: string;
git: string;
env: Record<string, string>;
createdAt: string;
status: AppStatus;
}

View File

@@ -0,0 +1,73 @@
import { Avatar, Layout, Menu } from '@arco-design/web-react';
import {
IconApps,
IconBulb,
IconFire,
IconMenuFold,
IconMenuUnfold,
IconRobot,
IconSafe,
IconUser,
} from '@arco-design/web-react/icon';
import { useState } from 'react';
import Logo from '@assets/images/logo.svg?react';
import { Outlet } from 'react-router';
function Home() {
const [collapsed, setCollapsed] = useState(false);
return (
<Layout className="h-screen w-full">
<Layout.Sider
collapsible
onCollapse={setCollapsed}
trigger={collapsed ? <IconMenuUnfold /> : <IconMenuFold />}
>
<div className="flex flex-row items-center justify-center px-2 py-3">
<Logo />
{!collapsed && <h2 className="ml-4 text-xl font-medium">Foka CI</h2>}
</div>
<Menu
className="flex-1"
defaultOpenKeys={['0']}
defaultSelectedKeys={['0_1']}
>
<Menu.Item key="0">
<IconApps />
Navigation 1
</Menu.Item>
<Menu.Item key="1">
<IconRobot />
Navigation 2
</Menu.Item>
<Menu.Item key="2">
<IconBulb />
Navigation 3
</Menu.Item>
<Menu.Item key="3">
<IconSafe />
Navigation 4
</Menu.Item>
<Menu.Item key="4">
<IconFire />
Navigation 5
</Menu.Item>
</Menu>
</Layout.Sider>
<Layout>
<Layout.Header className="h-14 border-b-gray-100 border-b-[1px]">
<div className="flex items-center justify-end px-4 h-full">
<Avatar>
<IconUser />
</Avatar>
</div>
</Layout.Header>
<Layout.Content>
<Outlet />
</Layout.Content>
</Layout>
</Layout>
);
}
export default Home;

View File

@@ -0,0 +1,18 @@
import { Input, Space } from '@arco-design/web-react';
import { IconUser, IconInfoCircle } from '@arco-design/web-react/icon';
function Login() {
return (
<div>
<Space direction='vertical'>
<Input placeholder="username" prefix={<IconUser />} size="large" />
<Input.Password
placeholder="password"
prefix={<IconInfoCircle />}
size="large"
/>
</Space>
</div>
);
}
export default Login;

View File

@@ -0,0 +1 @@
@import 'tailwindcss';

30
apps/web/tsconfig.json Normal file
View File

@@ -0,0 +1,30 @@
{
"compilerOptions": {
"lib": ["DOM", "ES2020"],
"jsx": "react-jsx",
"target": "ES2020",
"noEmit": true,
"skipLibCheck": true,
"useDefineForClassFields": true,
/* modules */
"module": "ESNext",
"moduleDetection": "force",
"moduleResolution": "bundler",
"verbatimModuleSyntax": true,
"resolveJsonModule": true,
"allowImportingTsExtensions": true,
"noUncheckedSideEffectImports": true,
/* type checking */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"paths": {
"@pages/*": ["./src/pages/*"],
"@styles/*": ["./src/styles/*"],
"@assets/*": ["./src/assets/*"]
}
},
"include": ["src"]
}