fix UI and add CI/CD
This commit is contained in:
parent
c39cb7a7ac
commit
9cd61df647
33
.gitea/workflows/ci.yml
Normal file
33
.gitea/workflows/ci.yml
Normal 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
|
||||||
34
.gitea/workflows/deploy-staging.yml
Normal file
34
.gitea/workflows/deploy-staging.yml
Normal 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
120
CI-CD-PLAN.md
Normal 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
|
||||||
|
|
@ -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"}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user