fix UI and add CI/CD
Some checks are pending
CI / build-test (push) Waiting to run
Deploy Staging (Docker) / deploy (push) Waiting to run

This commit is contained in:
Do Manh Phuong 2026-05-10 21:48:22 +07:00
parent c39cb7a7ac
commit 9cd61df647
6 changed files with 241 additions and 9 deletions

33
.gitea/workflows/ci.yml Normal file
View File

@ -0,0 +1,33 @@
name: CI
on:
push:
pull_request:
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22.14.0
cache: npm
- name: Install deps
run: npm ci
- name: Test
run: npm run test
- name: Build
run: npm run build
- name: Upload dist
uses: actions/upload-artifact@v4
with:
name: dist
path: dist

View File

@ -0,0 +1,34 @@
name: Deploy Staging (Docker)
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login Registry
run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ secrets.REGISTRY_URL }} -u ${{ secrets.REGISTRY_USER }} --password-stdin
- name: Build & Push
env:
IMAGE: ${{ secrets.REGISTRY_URL }}/${{ secrets.IMAGE_NAME }}
run: |
docker build --build-arg NODE_VERSION=22.14.0 -t $IMAGE:staging-${{ github.sha }} -t $IMAGE:staging-latest .
docker push $IMAGE:staging-${{ github.sha }}
docker push $IMAGE:staging-latest
- name: Deploy local
env:
IMAGE: ${{ secrets.REGISTRY_URL }}/${{ secrets.IMAGE_NAME }}
CONTAINER_NAME: ${{ secrets.CONTAINER_NAME }}
HOST_PORT: ${{ secrets.HOST_PORT }}
run: |
docker pull $IMAGE:staging-${{ github.sha }}
docker stop $CONTAINER_NAME || true
docker rm $CONTAINER_NAME || true
docker run -d --name $CONTAINER_NAME -p $HOST_PORT:80 $IMAGE:staging-${{ github.sha }}

120
CI-CD-PLAN.md Normal file
View File

@ -0,0 +1,120 @@
# Ke hoach CI/CD cho frontend (Gitea)
## 1) Tong quan
Ke hoach nay thiet lap:
- CI: build va test moi lan push/PR
- CD: deploy Docker tu dong len staging
## 2) Thong tin du an
- Build tool: Vite
- Script:
- test: `npm run test`
- build: `npm run build`
- Thu muc output: `dist`
## 3) Quy uoc da chot
1) Cach deploy: Docker
2) Node version: 22.14.0
3) Branch staging: `main`
4) Runner dat tren web server (deploy truc tiep, khong SSH)
## 4) Dieu kien truoc
- Web server Ubuntu 22.04 co Docker
- Web server truy cap duoc:
- Gitea: http://203.171.20.94:3000
- Docker Hub
## 5) Cai dat act_runner tren web server (Ubuntu 22.04)
### 5.1 Dung user compmanage lam runner
```bash
sudo usermod -aG docker compmanage
```
### 5.2 Tai act_runner
```bash
mkdir -p /home/compmanage/gitea-runner/bin
cd /home/compmanage/gitea-runner/bin
curl -L -o act_runner \
https://dl.gitea.com/act_runner/0.2.10/act_runner-0.2.10-linux-amd64
chmod +x act_runner
```
### 5.3 Lay token va dang ky runner
- Gitea Admin -> Actions -> Runners -> Create new runner
- Dang ky runner:
```bash
/home/compmanage/gitea-runner/bin/act_runner register \
--instance http://203.171.20.94:3000 \
--token EcAi7bNFMbDf1OcwTCem3dyFXOao1IW9Qfs1Ugb6 \
--name webserver-runner \
--labels docker:docker://node:22.14.0
```
### 5.4 Chay runner bang systemd
Tao file `/etc/systemd/system/act_runner.service`:
```ini
[Unit]
Description=Gitea Actions Runner
After=network.target
[Service]
User=compmanage
Group=compmanage
ExecStart=/home/compmanage/gitea-runner/bin/act_runner daemon
WorkingDirectory=/home/compmanage/gitea-runner
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
```
Khoi dong:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now act_runner
sudo systemctl status act_runner
```
## 6) Secrets can tao trong Gitea
Vao repo -> Settings -> Actions -> Secrets, them cac key sau:
- REGISTRY_URL = docker.io
- REGISTRY_USER = <dockerhub_username>
- REGISTRY_PASSWORD = <dockerhub_access_token>
- IMAGE_NAME = <dockerhub_username>/ttmt-frontend
- CONTAINER_NAME = ttmt-frontend
- HOST_PORT = 80
## 7) Workflow can co
- .gitea/workflows/ci.yml
- .gitea/workflows/deploy-staging.yml
## 8) Luong CI (build + test)
- Trigger: push, pull_request
- Cac buoc:
1) Checkout
2) Setup Node 22.14.0
3) npm ci
4) npm run test
5) npm run build
6) Upload dist artifact
## 9) Luong CD (deploy Docker truc tiep)
- Trigger: push len branch `main`
- Cac buoc:
1) Build image tu Dockerfile
2) Push len Docker Hub
3) Pull va restart container tren chinh web server
## 10) Kiem tra sau khi deploy
- Gitea -> Actions: job thanh cong
- Tren server: `docker ps` thay container dang chay
- Truy cap web bang IP/port da map
## 11) Rollback nhanh
- Deploy lai tag cu: `staging-<commit SHA>`
## 12) Buoc tiep theo
1) Them secrets tren Gitea
2) Push len `main` de trigger deploy

View File

@ -67,6 +67,17 @@ function CommandPage() {
const deleteCommandMutation = useDeleteCommand(); const deleteCommandMutation = useDeleteCommand();
const sendCommandMutation = useSendCommand(); const sendCommandMutation = useSendCommand();
const formInitialData = selectedCommand
? {
commandName: selectedCommand.commandName,
commandType: selectedCommand.commandType,
description: selectedCommand.description,
commandContent: selectedCommand.commandContent,
qos: selectedCommand.qoS,
isRetained: selectedCommand.isRetained,
}
: undefined;
// Columns for command table // Columns for command table
const columns: ColumnDef<CommandRegistry>[] = [ const columns: ColumnDef<CommandRegistry>[] = [
{ {
@ -303,7 +314,7 @@ function CommandPage() {
<CommandRegistryForm <CommandRegistryForm
onSubmit={handleFormSubmit} onSubmit={handleFormSubmit}
closeDialog={() => setIsDialogOpen(false)} closeDialog={() => setIsDialogOpen(false)}
initialData={selectedCommand || undefined} initialData={formInitialData}
title={selectedCommand ? "Sửa Lệnh" : "Thêm Lệnh Mới"} title={selectedCommand ? "Sửa Lệnh" : "Thêm Lệnh Mới"}
/> />
} }

View File

@ -5,7 +5,7 @@ import {
} from "@tanstack/react-router"; } from "@tanstack/react-router";
import { useMemo } from "react"; import { useMemo } from "react";
import { useGetClientFolderStatus } from "@/hooks/queries"; import { useGetClientFolderStatus } from "@/hooks/queries";
import type { ClientFolderStatus } from "@/types/folder"; import type { ClientFolderStatus, ExtraFile, MissingFile } from "@/types/folder";
import FolderStatusTemplate from "@/template/folder-status-template"; import FolderStatusTemplate from "@/template/folder-status-template";
import { import {
createColumnHelper, createColumnHelper,
@ -57,6 +57,25 @@ function RouteComponent() {
const columnHelper = createColumnHelper<ClientFolderStatus>(); const columnHelper = createColumnHelper<ClientFolderStatus>();
const renderFileList = (files?: MissingFile[] | ExtraFile[]) => {
if (!files || files.length === 0) {
return <span className="text-muted-foreground">-</span>;
}
return (
<div className="max-h-32 overflow-auto space-y-1">
{files.map((file) => (
<div key={`${file.folderPath}/${file.fileName}`} className="text-xs">
<div className="font-mono break-all">{file.fileName}</div>
<div className="text-muted-foreground break-all">
{file.folderPath}
</div>
</div>
))}
</div>
);
};
const columns = useMemo( const columns = useMemo(
() => [ () => [
columnHelper.accessor("deviceId", { columnHelper.accessor("deviceId", {
@ -65,14 +84,13 @@ function RouteComponent() {
}), }),
columnHelper.display({ columnHelper.display({
id: "missing", id: "missing",
header: "Số lượng file thiếu", header: "File thiếu",
cell: (info) => cell: (info) => renderFileList(info.row.original.missingFiles),
(info.row.original.missingFiles?.length ?? 0).toString(),
}), }),
columnHelper.display({ columnHelper.display({
id: "extra", id: "extra",
header: "Số lượng file thừa", header: "File thừa",
cell: (info) => (info.row.original.extraFiles?.length ?? 0).toString(), cell: (info) => renderFileList(info.row.original.extraFiles),
}), }),
columnHelper.display({ columnHelper.display({
id: "current", id: "current",

View File

@ -14,7 +14,7 @@ import { RequestUpdateMenu } from "@/components/menu/request-update-menu";
import { DeleteMenu } from "@/components/menu/delete-menu"; import { DeleteMenu } from "@/components/menu/delete-menu";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import type { AxiosProgressEvent } from "axios"; import type { AxiosProgressEvent } from "axios";
import { useState } from "react"; import { useMemo, useState } from "react";
import { SelectDialog } from "@/components/dialogs/select-dialog"; import { SelectDialog } from "@/components/dialogs/select-dialog";
import { DeviceSearchDialog } from "@/components/bars/device-searchbar"; import { DeviceSearchDialog } from "@/components/bars/device-searchbar";
import { UploadVersionForm } from "@/components/forms/upload-file-form"; import { UploadVersionForm } from "@/components/forms/upload-file-form";
@ -80,6 +80,22 @@ export function AppManagerTemplate<TData>({
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const [dialogType, setDialogType] = useState<"room" | "device" | "download-room" | "download-device" | null>(null); const [dialogType, setDialogType] = useState<"room" | "device" | "download-room" | "download-device" | null>(null);
const sortedData = useMemo(() => {
const firstItem = data?.[0] as { fileName?: string } | undefined;
if (!firstItem || typeof firstItem.fileName !== "string") {
return data;
}
return [...data].sort((a, b) => {
const aName = (a as { fileName?: string }).fileName ?? "";
const bName = (b as { fileName?: string }).fileName ?? "";
return aName.localeCompare(bName, "vi", {
numeric: true,
sensitivity: "base",
});
});
}, [data]);
const openRoomDialog = () => { const openRoomDialog = () => {
if (rooms.length > 0 && onUpdate) { if (rooms.length > 0 && onUpdate) {
setDialogType("room"); setDialogType("room");
@ -149,7 +165,7 @@ export function AppManagerTemplate<TData>({
<CardContent> <CardContent>
<VersionTable <VersionTable
data={data} data={sortedData}
isLoading={isLoading} isLoading={isLoading}
columns={columns} columns={columns}
onTableInit={onTableInit} onTableInit={onTableInit}