diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..8919e64 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -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 diff --git a/.gitea/workflows/deploy-staging.yml b/.gitea/workflows/deploy-staging.yml new file mode 100644 index 0000000..907029e --- /dev/null +++ b/.gitea/workflows/deploy-staging.yml @@ -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 }} diff --git a/CI-CD-PLAN.md b/CI-CD-PLAN.md new file mode 100644 index 0000000..55bedf8 --- /dev/null +++ b/CI-CD-PLAN.md @@ -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 = +- REGISTRY_PASSWORD = +- IMAGE_NAME = /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-` + +## 12) Buoc tiep theo +1) Them secrets tren Gitea +2) Push len `main` de trigger deploy diff --git a/src/routes/_auth/commands/index.tsx b/src/routes/_auth/commands/index.tsx index ccba3e6..635cb9f 100644 --- a/src/routes/_auth/commands/index.tsx +++ b/src/routes/_auth/commands/index.tsx @@ -67,6 +67,17 @@ function CommandPage() { const deleteCommandMutation = useDeleteCommand(); 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 const columns: ColumnDef[] = [ { @@ -303,7 +314,7 @@ function CommandPage() { setIsDialogOpen(false)} - initialData={selectedCommand || undefined} + initialData={formInitialData} title={selectedCommand ? "Sửa Lệnh" : "Thêm Lệnh Mới"} /> } diff --git a/src/routes/_auth/rooms/$roomName/folder-status/index.tsx b/src/routes/_auth/rooms/$roomName/folder-status/index.tsx index 8acee7a..d1e33a9 100644 --- a/src/routes/_auth/rooms/$roomName/folder-status/index.tsx +++ b/src/routes/_auth/rooms/$roomName/folder-status/index.tsx @@ -5,7 +5,7 @@ import { } from "@tanstack/react-router"; import { useMemo } from "react"; 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 { createColumnHelper, @@ -57,6 +57,25 @@ function RouteComponent() { const columnHelper = createColumnHelper(); + const renderFileList = (files?: MissingFile[] | ExtraFile[]) => { + if (!files || files.length === 0) { + return -; + } + + return ( +
+ {files.map((file) => ( +
+
{file.fileName}
+
+ {file.folderPath} +
+
+ ))} +
+ ); + }; + const columns = useMemo( () => [ columnHelper.accessor("deviceId", { @@ -65,14 +84,13 @@ function RouteComponent() { }), columnHelper.display({ id: "missing", - header: "Số lượng file thiếu", - cell: (info) => - (info.row.original.missingFiles?.length ?? 0).toString(), + header: "File thiếu", + cell: (info) => renderFileList(info.row.original.missingFiles), }), columnHelper.display({ id: "extra", - header: "Số lượng file thừa", - cell: (info) => (info.row.original.extraFiles?.length ?? 0).toString(), + header: "File thừa", + cell: (info) => renderFileList(info.row.original.extraFiles), }), columnHelper.display({ id: "current", diff --git a/src/template/app-manager-template.tsx b/src/template/app-manager-template.tsx index 9cc9e7f..b51e71c 100644 --- a/src/template/app-manager-template.tsx +++ b/src/template/app-manager-template.tsx @@ -14,7 +14,7 @@ import { RequestUpdateMenu } from "@/components/menu/request-update-menu"; import { DeleteMenu } from "@/components/menu/delete-menu"; import { Button } from "@/components/ui/button"; import type { AxiosProgressEvent } from "axios"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { SelectDialog } from "@/components/dialogs/select-dialog"; import { DeviceSearchDialog } from "@/components/bars/device-searchbar"; import { UploadVersionForm } from "@/components/forms/upload-file-form"; @@ -80,6 +80,22 @@ export function AppManagerTemplate({ const [dialogOpen, setDialogOpen] = useState(false); 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 = () => { if (rooms.length > 0 && onUpdate) { setDialogType("room"); @@ -149,7 +165,7 @@ export function AppManagerTemplate({