Merge pull request #231 from kimatata/develop

This commit is contained in:
kimatata
2025-05-25 21:24:57 +09:00
committed by GitHub
30 changed files with 30012 additions and 27327 deletions

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
node_modules/ node_modules/
coverage/ coverage/
.env .env
database.sqlite
/test-results/ /test-results/
/playwright-report/ /playwright-report/
/playwright-screenshots/ /playwright-screenshots/

71
Dockerfile Normal file
View File

@@ -0,0 +1,71 @@
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install root dependencies
COPY package.json package-lock.json* ./
RUN npm ci
# Install frontend dependencies
WORKDIR /app/frontend
COPY frontend/package.json frontend/package-lock.json* ./
RUN npm ci
# Install backend dependencies
WORKDIR /app/backend
COPY backend/package.json backend/package-lock.json* ./
RUN npm ci
# Build frontend
FROM base AS frontend-builder
WORKDIR /app
COPY package.json ./
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/frontend/node_modules ./frontend/node_modules
COPY frontend ./frontend
WORKDIR /app/frontend
RUN npm run build
# Final production image
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=8000
ENV FRONTEND_ORIGIN=http://localhost:8000
# Create a non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy backend files
COPY backend ./backend
# Copy frontend build
COPY --from=frontend-builder /app/frontend/.next/standalone ./
COPY --from=frontend-builder /app/frontend/.next/static ./.next/static
COPY --from=frontend-builder /app/frontend/public ./public
# Copy Next.js module for the server
COPY --from=deps /app/frontend/node_modules/next ./node_modules/next
# Install backend dependencies directly in the container to ensure native modules are built correctly
WORKDIR /app/backend
COPY backend/package.json backend/package-lock.json* ./
RUN npm ci
WORKDIR /app
# Copy custom combined_server.js that combines frontend and backend
COPY combined_server.js ./
# Expose the port
EXPOSE 8000
# Run database migrations and start the combined server
CMD ["node", "combined_server.js"]

1
backend/.gitignore vendored
View File

@@ -1,4 +1,3 @@
node_modules/ node_modules/
.env .env
database.sqlite
public/uploads/ public/uploads/

View File

@@ -1,11 +0,0 @@
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
EXPOSE $PORT
RUN npx sequelize db:migrate
CMD ["node", "index.js"]

18
backend/config/config.js Normal file
View File

@@ -0,0 +1,18 @@
const path = require('path');
const databasePath = path.resolve(__dirname, '../database/database.sqlite');
module.exports = {
development: {
dialect: 'sqlite',
storage: databasePath,
},
test: {
dialect: 'sqlite',
storage: databasePath,
},
production: {
dialect: 'sqlite',
storage: databasePath,
},
};

View File

@@ -1,14 +0,0 @@
{
"development": {
"dialect": "sqlite",
"storage": "database/database.sqlite"
},
"test": {
"dialect": "sqlite",
"storage": "database/database.sqlite"
},
"production": {
"dialect": "sqlite",
"storage": "database/database.sqlite"
}
}

View File

@@ -1,153 +1,7 @@
const express = require('express'); const server = require('./server');
const RateLimit = require('express-rate-limit');
const path = require('path');
const { Sequelize } = require('sequelize');
const app = express();
// enable frontend access
const cors = require('cors');
const frontendOrigin = process.env.FRONTEND_ORIGIN || 'http://localhost:8000';
const corsOptions = {
origin: frontendOrigin,
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
};
app.use(cors(corsOptions));
// enable json middleware
app.use(express.json());
// enable rate limiter
const limiter = RateLimit({
windowMs: 60 * 60 * 1000, // 1h
max: 1000, // 1000 requests per hour
message: 'Too many requests from this IP, please try again after an hour',
});
app.use(limiter);
// Specify the directory to serve static files
app.use(express.static(path.join(__dirname, 'public')));
// init sequalize
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: 'database/database.sqlite',
logging: false,
});
// "/"
const indexRoute = require('./routes/index');
app.use('/', indexRoute);
// "users"
const usersIndexRoute = require('./routes/users/index')(sequelize);
const usersFindRoute = require('./routes/users/find')(sequelize);
const usersSearchRoute = require('./routes/users/search')(sequelize);
const signUpRoute = require('./routes/users/signup')(sequelize);
const signInRoute = require('./routes/users/signin')(sequelize);
app.use('/users', usersIndexRoute);
app.use('/users', usersFindRoute);
app.use('/users', usersSearchRoute);
app.use('/users', signUpRoute);
app.use('/users', signInRoute);
// ESM import
(async () => {
const updateRoute = await import('./routes/users/update.mjs');
app.use('/users', updateRoute.default(sequelize));
})();
// "/projects"
const projectsIndexRoute = require('./routes/projects/index')(sequelize);
const projectsShowRoute = require('./routes/projects/show')(sequelize);
const projectsNewRoute = require('./routes/projects/new')(sequelize);
const projectsEditRoute = require('./routes/projects/edit')(sequelize);
const projectsDeleteRoute = require('./routes/projects/delete')(sequelize);
app.use('/projects', projectsIndexRoute);
app.use('/projects', projectsShowRoute);
app.use('/projects', projectsNewRoute);
app.use('/projects', projectsEditRoute);
app.use('/projects', projectsDeleteRoute);
// "/folders"
const foldersIndexRoute = require('./routes/folders/index')(sequelize);
const foldersNewRoute = require('./routes/folders/new')(sequelize);
const foldersEditRoute = require('./routes/folders/edit')(sequelize);
const foldersDeleteRoute = require('./routes/folders/delete')(sequelize);
app.use('/folders', foldersIndexRoute);
app.use('/folders', foldersNewRoute);
app.use('/folders', foldersEditRoute);
app.use('/folders', foldersDeleteRoute);
// "/cases"
const casesDownloadRoute = require('./routes/cases/download')(sequelize);
const casesIndexRoute = require('./routes/cases/index')(sequelize);
const casesIndexByProjectIdRoute = require('./routes/cases/indexByProjectId')(sequelize);
const casesShowRoute = require('./routes/cases/show')(sequelize);
const casesNewRoute = require('./routes/cases/new')(sequelize);
const casesEditRoute = require('./routes/cases/edit')(sequelize);
const casesDeleteRoute = require('./routes/cases/delete')(sequelize);
app.use('/cases', casesDownloadRoute);
app.use('/cases', casesIndexRoute);
app.use('/cases', casesIndexByProjectIdRoute);
app.use('/cases', casesShowRoute);
app.use('/cases', casesNewRoute);
app.use('/cases', casesEditRoute);
app.use('/cases', casesDeleteRoute);
// "/steps"
const stepsEditRoute = require('./routes/steps/edit')(sequelize);
app.use('/steps', stepsEditRoute);
// "/attachments"
const attachmentsNewRoute = require('./routes/attachments/new')(sequelize);
const attachmentsDeleteRoute = require('./routes/attachments/delete')(sequelize);
const attachmentsDownloadRoute = require('./routes/attachments/download')(sequelize);
app.use('/attachments', attachmentsNewRoute);
app.use('/attachments', attachmentsDeleteRoute);
app.use('/attachments', attachmentsDownloadRoute);
// "/runs"
const runsDownloadRoute = require('./routes/runs/download')(sequelize);
const runsIndexRoute = require('./routes/runs/index')(sequelize);
const runsShowRoute = require('./routes/runs/show')(sequelize);
const runsNewRoute = require('./routes/runs/new')(sequelize);
const runsEditRoute = require('./routes/runs/edit')(sequelize);
const runDeleteRoute = require('./routes/runs/delete')(sequelize);
app.use('/runs', runsDownloadRoute);
app.use('/runs', runsIndexRoute);
app.use('/runs', runsShowRoute);
app.use('/runs', runsNewRoute);
app.use('/runs', runsEditRoute);
app.use('/runs', runDeleteRoute);
// "/runcases"
const runCaseIndexRoute = require('./routes/runcases/index')(sequelize);
const runCaseEditRoute = require('./routes/runcases/edit')(sequelize);
app.use('/runcases', runCaseIndexRoute);
app.use('/runcases', runCaseEditRoute);
// "/members"
const membersIndexRoute = require('./routes/members/index')(sequelize);
const membersNewRoute = require('./routes/members/new')(sequelize);
const membersEditRoute = require('./routes/members/edit')(sequelize);
const membersDeleteRoute = require('./routes/members/delete')(sequelize);
const membersCheckRoute = require('./routes/members/check')(sequelize);
app.use('/members', membersIndexRoute);
app.use('/members', membersNewRoute);
app.use('/members', membersEditRoute);
app.use('/members', membersDeleteRoute);
app.use('/members', membersCheckRoute);
// "/home"
const homeIndexRoute = require('./routes/home/index')(sequelize);
app.use('/home', homeIndexRoute);
const PORT = process.env.PORT || 8001; const PORT = process.env.PORT || 8001;
app.listen(PORT, () => {
server.listen(PORT, () => {
console.log(`Backend server is running on port ${PORT}`); console.log(`Backend server is running on port ${PORT}`);
console.log(`Access from the frontend origin: ${frontendOrigin} is valid.`);
if (!process.env.SECRET_KEY) {
console.log(
"[Warning]: Default key is used for token generation. Please set the environment variable 'SECRET_KEY'`."
);
}
}); });

View File

@@ -5,9 +5,9 @@
"scripts": { "scripts": {
"start": "node --env-file=.env index", "start": "node --env-file=.env index",
"dev": "node index", "dev": "node index",
"migrate": "bash scripts/migrate.sh", "migrate": "sequelize db:migrate",
"seed": "bash scripts/seed.sh", "seed": "sequelize db:seed:all",
"drop": "bash scripts/drop.sh" "drop": "sequelize db:migrate:undo:all"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",

View File

@@ -1,4 +0,0 @@
#!/bin/bash
npx sequelize db:migrate:undo:all
echo "Dropped all tables"

View File

@@ -1,4 +0,0 @@
#!/bin/bash
npx sequelize db:migrate
echo "Migration complete."

View File

@@ -1,3 +0,0 @@
#!/bin/bash
npx sequelize db:seed:all

153
backend/server.js Normal file
View File

@@ -0,0 +1,153 @@
const path = require('path');
const express = require('express');
const RateLimit = require('express-rate-limit');
const { Sequelize } = require('sequelize');
const app = express();
// enable frontend access
const cors = require('cors');
const frontendOrigin = process.env.FRONTEND_ORIGIN || 'http://localhost:8000';
const corsOptions = {
origin: frontendOrigin,
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
};
app.use(cors(corsOptions));
// enable json middleware
app.use(express.json());
// enable rate limiter
const limiter = RateLimit({
windowMs: 60 * 60 * 1000, // 1h
max: 1000, // 1000 requests per hour
message: 'Too many requests from this IP, please try again after an hour',
});
app.use(limiter);
// Specify the directory to serve static files
app.use(express.static(path.join(__dirname, 'public')));
// init sequalize
const databasePath = path.resolve(__dirname, 'database/database.sqlite');
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: databasePath,
logging: false,
});
// "/"
const indexRoute = require('./routes/index');
app.use('/', indexRoute);
// "users"
const usersIndexRoute = require('./routes/users/index')(sequelize);
const usersFindRoute = require('./routes/users/find')(sequelize);
const usersSearchRoute = require('./routes/users/search')(sequelize);
const signUpRoute = require('./routes/users/signup')(sequelize);
const signInRoute = require('./routes/users/signin')(sequelize);
app.use('/users', usersIndexRoute);
app.use('/users', usersFindRoute);
app.use('/users', usersSearchRoute);
app.use('/users', signUpRoute);
app.use('/users', signInRoute);
// ESM import
(async () => {
const updateRoute = await import('./routes/users/update.mjs');
app.use('/users', updateRoute.default(sequelize));
})();
// "/projects"
const projectsIndexRoute = require('./routes/projects/index')(sequelize);
const projectsShowRoute = require('./routes/projects/show')(sequelize);
const projectsNewRoute = require('./routes/projects/new')(sequelize);
const projectsEditRoute = require('./routes/projects/edit')(sequelize);
const projectsDeleteRoute = require('./routes/projects/delete')(sequelize);
app.use('/projects', projectsIndexRoute);
app.use('/projects', projectsShowRoute);
app.use('/projects', projectsNewRoute);
app.use('/projects', projectsEditRoute);
app.use('/projects', projectsDeleteRoute);
// "/folders"
const foldersIndexRoute = require('./routes/folders/index')(sequelize);
const foldersNewRoute = require('./routes/folders/new')(sequelize);
const foldersEditRoute = require('./routes/folders/edit')(sequelize);
const foldersDeleteRoute = require('./routes/folders/delete')(sequelize);
app.use('/folders', foldersIndexRoute);
app.use('/folders', foldersNewRoute);
app.use('/folders', foldersEditRoute);
app.use('/folders', foldersDeleteRoute);
// "/cases"
// "/cases"
const casesDownloadRoute = require('./routes/cases/download')(sequelize);
const casesIndexRoute = require('./routes/cases/index')(sequelize);
const casesIndexByProjectIdRoute = require('./routes/cases/indexByProjectId')(sequelize);
const casesShowRoute = require('./routes/cases/show')(sequelize);
const casesNewRoute = require('./routes/cases/new')(sequelize);
const casesEditRoute = require('./routes/cases/edit')(sequelize);
const casesDeleteRoute = require('./routes/cases/delete')(sequelize);
app.use('/cases', casesDownloadRoute);
app.use('/cases', casesIndexRoute);
app.use('/cases', casesIndexByProjectIdRoute);
app.use('/cases', casesShowRoute);
app.use('/cases', casesNewRoute);
app.use('/cases', casesEditRoute);
app.use('/cases', casesDeleteRoute);
// "/steps"
const stepsEditRoute = require('./routes/steps/edit')(sequelize);
app.use('/steps', stepsEditRoute);
// "/attachments"
const attachmentsNewRoute = require('./routes/attachments/new')(sequelize);
const attachmentsDeleteRoute = require('./routes/attachments/delete')(sequelize);
const attachmentsDownloadRoute = require('./routes/attachments/download')(sequelize);
app.use('/attachments', attachmentsNewRoute);
app.use('/attachments', attachmentsDeleteRoute);
app.use('/attachments', attachmentsDownloadRoute);
// "/runs"
const runsDownloadRoute = require('./routes/runs/download')(sequelize);
const runsIndexRoute = require('./routes/runs/index')(sequelize);
const runsShowRoute = require('./routes/runs/show')(sequelize);
const runsNewRoute = require('./routes/runs/new')(sequelize);
const runsEditRoute = require('./routes/runs/edit')(sequelize);
const runDeleteRoute = require('./routes/runs/delete')(sequelize);
app.use('/runs', runsDownloadRoute);
app.use('/runs', runsIndexRoute);
app.use('/runs', runsShowRoute);
app.use('/runs', runsNewRoute);
app.use('/runs', runsEditRoute);
app.use('/runs', runDeleteRoute);
// "/runcases"
const runCaseIndexRoute = require('./routes/runcases/index')(sequelize);
const runCaseEditRoute = require('./routes/runcases/edit')(sequelize);
app.use('/runcases', runCaseIndexRoute);
app.use('/runcases', runCaseEditRoute);
// "/members"
const membersIndexRoute = require('./routes/members/index')(sequelize);
const membersNewRoute = require('./routes/members/new')(sequelize);
const membersEditRoute = require('./routes/members/edit')(sequelize);
const membersDeleteRoute = require('./routes/members/delete')(sequelize);
const membersCheckRoute = require('./routes/members/check')(sequelize);
app.use('/members', membersIndexRoute);
app.use('/members', membersNewRoute);
app.use('/members', membersEditRoute);
app.use('/members', membersDeleteRoute);
app.use('/members', membersCheckRoute);
// "/home"
const homeIndexRoute = require('./routes/home/index')(sequelize);
app.use('/home', homeIndexRoute);
if (!process.env.SECRET_KEY) {
console.log(
"[Warning]: Default key is used for token generation. Please set the environment variable 'SECRET_KEY'`."
);
}
// Export the app instead of starting the server
module.exports = app;

87
combined_server.js Normal file
View File

@@ -0,0 +1,87 @@
// Use express from backend node_modules
const express = require('./backend/node_modules/express');
const { createServer: createHttpServer } = require('http');
const path = require('path');
const fs = require('fs');
const { execSync } = require('child_process');
async function runMigrations() {
try {
console.log('Running database migrations...');
// Use execSync with cwd option to run in the backend directory
execSync('npx sequelize-cli db:migrate', {
cwd: path.join(__dirname, 'backend'),
stdio: 'inherit',
});
console.log('Database migrations completed successfully.');
if (process.env.IS_DEMO === 'true' || process.env.IS_DEMO === '1') {
console.log('Demo mode detected. Seeding the database...');
execSync('npx sequelize-cli db:seed:all', {
cwd: path.join(__dirname, 'backend'),
stdio: 'inherit',
});
console.log('Database seeding completed successfully.');
}
} catch (error) {
console.error('Error running database migrations or seeding:', error);
throw error;
}
}
function startServer() {
try {
const server = express();
const httpServer = createHttpServer(server);
// Import the backend app
const backendApp = require('./backend/server');
// Use the backend app for /api routes
server.use('/api', backendApp);
// For Next.js standalone build
// Check if we have the Next.js server file
const nextServerPath = './node_modules/next/dist/server/next.js';
if (fs.existsSync(nextServerPath)) {
// Import Next.js
const next = require(nextServerPath);
// Initialize Next.js app
const dev = process.env.NODE_ENV !== 'production';
const nextApp = next({ dev, dir: path.join(__dirname, '.') });
const handle = nextApp.getRequestHandler();
// Prepare Next.js
nextApp.prepare().then(() => {
console.log('nextjs prepared');
// Use Next.js to handle all other routes
server.all('*', (req, res) => {
return handle(req, res);
});
});
} else {
console.error('Next.js module not found at:', nextServerPath);
server.all('*', (req, res) => {
res.status(500).send('Frontend server not available');
});
}
const PORT = process.env.PORT || 8000;
httpServer.listen(PORT, (err) => {
if (err) throw err;
console.log(`> Ready on http://localhost:${PORT}`);
});
} catch (error) {
console.error('Error starting server:', error);
process.exit(1);
}
}
runMigrations().then(() => {
startServer();
}).catch(error => {
console.error('Failed to start application:', error);
process.exit(1);
});

View File

@@ -1,31 +1,15 @@
version: '3.8'
services: services:
backend: unittcms:
image: unittcms-backend:latest image: unittcms:latest
build: build: .
context: ./backend
ports:
- '8001:8001'
environment:
- NODE_ENV=production
- FRONTEND_ORIGIN=http://localhost:8000
- PORT=8001
volumes:
- db-data:/app/database
frontend:
image: unittcms-frontend:latest
build:
context: ./frontend
ports: ports:
- '8000:8000' - '8000:8000'
environment: environment:
- NODE_ENV=production
- NEXT_PUBLIC_BACKEND_ORIGIN=http://localhost:8001
- PORT=8000 - PORT=8000
depends_on: - SECRET_KEY=your_secret_key_here
- backend - IS_DEMO=false # set to true to seed the database
volumes:
- db-data:/app/backend/database
volumes: volumes:
db-data: db-data:

View File

@@ -1,5 +1,6 @@
--- ---
sidebar_position: 4 sidebar_position: 5
draft: true
--- ---
# Deployment # Deployment

View File

@@ -10,7 +10,7 @@ First, clone the repository.
git clone https://github.com/kimatata/unittcms.git git clone https://github.com/kimatata/unittcms.git
``` ```
and start containers with the following command. and start container with the following command.
```bash ```bash
cd unittcms cd unittcms

View File

@@ -1,28 +1,56 @@
--- ---
sidebar_position: 5 sidebar_position: 4
--- ---
# Override settings # Override settings
The system will work with default settings without setting environment variables, but you can override the settings by placing `.env` file. UnitTCMS will work with the default settings, but you can override them as needed.
## setting frontend environment variables :::warning[Strongly Recommended]
Create `.env` File on `frontend/` It is strongly recommended to change `SECRET_KEY` from the default value in production.
:::
## Docker
If you are self-hosting UnitTCMS with Docker, you can customize the environment using the `environment` section in `docker-compose.yaml`.
```yaml title="docker-compose.yaml"
services:
unittcms:
image: unittcms:latest
build: .
ports:
- '8000:8000'
// highlight-start
environment:
- PORT=8000
- SECRET_KEY=your_secret_key_here
- IS_DEMO=false # set to true to seed the database
// highlight-end
volumes:
- db-data:/app/backend/database
volumes:
db-data:
```
## From Source
If you are self-hosting UnitTCMS from source, you can override the environment by placing `.env` files in the appropriate directory.
### Setting frontend environment variables
Create a `.env` file in the `frontend/` directory:
```.env title="frontend/.env" ```.env title="frontend/.env"
NEXT_PUBLIC_BACKEND_ORIGIN=http://localhost:8001 NEXT_PUBLIC_BACKEND_ORIGIN=http://localhost:8001
``` ```
## setting backend environment variables ### Setting backend environment variables
Create `.env` File on `backend/` Create a `.env` file in the `backend/` directory:
:::warning[Strongly Recommended]
It is strongly recommended to set SECRET_KEY in production.
:::
```.env title="backend/.env" ```.env title="backend/.env"
FRONTEND_ORIGIN=http://localhost:8000 FRONTEND_ORIGIN=http://localhost:8000

View File

@@ -0,0 +1,75 @@
---
sidebar_position: 3
---
# Running UnitTCMS from Source
While using Docker is the recommended and easiest way to run UnitTCMS, this chapter explains how to run it directly from source. This approach is useful for those who cannot use Docker or are interested in contributing to UnitTCMS development. Since the frontend and backend are completely decoupled, this setup also supports more advanced use cases — such as serving the frontend via AWS S3 + CloudFront, or replacing the default SQLite database with another backend solution.
:::info[Prerequisite]
Prerequisite: v20 or higher node must be installed.
:::
To use UnitTCMS, you need to run both frontend server and backend(API) server.
First, clone the repository.
```bash
git clone https://github.com/kimatata/unittcms.git
```
## Run backend server
Place the .env file at `backend/.env`.
```.env title="backend/.env"
FRONTEND_ORIGIN=http://localhost:8000
```
Move to backend directory, then install dependencies.
```bash
cd backend
npm install
```
Initialize the database with the following command.
```bash
npm run migrate
```
Start backend server.
```bash
npm run start
```
## Run frontend server
Move to frontend directory, then install dependencies.
Place the .env file at `frontend/.env`.
```.env title="frontend/.env"
NEXT_PUBLIC_BACKEND_ORIGIN=http://localhost:8001
```
```bash
cd frontend
npm install
```
Build frontend code
```bash
npm run build
```
Start frontend server
```bash
npm run start
```

View File

@@ -1,61 +0,0 @@
---
sidebar_position: 3
---
# Running UnitTCMS manually
:::info[Prerequisite]
Prerequisite: v20 or higher node must be installed.
:::
To use UnitTCMS, you need run frontend server and backend(API) server.
First, clone the repository.
```bash
git clone https://github.com/kimatata/unittcms.git
```
## Run backend server
Move to backend directory, then install dependencies.
```bash
cd backend
npm ci
```
Initialize the database with the following command.
```bash
npm run migrate
```
Start backend server.
```bash
npm run start
```
## Run frontend server
Move to frontend directory, then install dependencies.
```bash
cd frontend
npm ci
```
Build frontend code
```bash
npm run build
```
Start frontend server
```
npm run start
```

View File

@@ -7,4 +7,4 @@ sidebar_position: 1
UnitTCMS is designed for self-hosted use. There are two ways to self-host the application: “Docker” and “Manual”. UnitTCMS is designed for self-hosted use. There are two ways to self-host the application: “Docker” and “Manual”.
- Docker (Recommended) - Docker (Recommended)
- Manual - From Source

12614
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,11 +14,11 @@
"write-heading-ids": "docusaurus write-heading-ids" "write-heading-ids": "docusaurus write-heading-ids"
}, },
"dependencies": { "dependencies": {
"@docusaurus/core": "^3.4.0", "@docusaurus/core": "^3.7.0",
"@docusaurus/module-type-aliases": "^3.4.0", "@docusaurus/module-type-aliases": "^3.7.0",
"@docusaurus/preset-classic": "^3.5.2", "@docusaurus/preset-classic": "^3.7.0",
"@docusaurus/theme-mermaid": "^3.5.2", "@docusaurus/theme-mermaid": "^3.7.0",
"@docusaurus/types": "^3.4.0", "@docusaurus/types": "^3.7.0",
"@mdx-js/react": "^3.1.0", "@mdx-js/react": "^3.1.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"prism-react-renderer": "^2.4.0", "prism-react-renderer": "^2.4.0",

View File

@@ -1,6 +0,0 @@
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
await page.goto('http://localhost:8000/');
await expect(page).toHaveTitle('Open Source Test Case Management System | UnitTCMS');
});

12
e2e/not-demo-mode.spec.ts Normal file
View File

@@ -0,0 +1,12 @@
import { test, expect } from '@playwright/test';
test('If not demo mode, the landing page will not be displayed and will be redirected to the projects page.', async ({
page,
}) => {
const screenshotDir = 'playwright-screenshots/not-demo-redirect';
await page.goto('http://localhost:8000/');
await expect(page).toHaveURL(/\/projects/);
await page.screenshot({ path: `${screenshotDir}/landing-page-redirect.png`, fullPage: true });
});

View File

@@ -1,64 +0,0 @@
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 8000
# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
CMD HOSTNAME="0.0.0.0" node server.js

View File

@@ -1,5 +1,5 @@
const Config = { const Config = {
apiServer: process.env.NEXT_PUBLIC_BACKEND_ORIGIN || 'http://localhost:8001', apiServer: process.env.NEXT_PUBLIC_BACKEND_ORIGIN || '/api',
// set 'NEXT_PUBLIC_IS_DEMO=true' in frontend/.env // set 'NEXT_PUBLIC_IS_DEMO=true' in frontend/.env
isDemoSite: process.env.NEXT_PUBLIC_IS_DEMO === 'true' || false, isDemoSite: process.env.NEXT_PUBLIC_IS_DEMO === 'true' || false,

View File

@@ -84,7 +84,7 @@
"languages": "Languages" "languages": "Languages"
}, },
"Toast": { "Toast": {
"need_signed_in": "You need signed in", "need_signed_in": "You need to sign in",
"session_expired": "Session expired" "session_expired": "Session expired"
}, },
"Auth": { "Auth": {

View File

@@ -24,6 +24,7 @@ import DropdownAccount from './DropdownAccount';
import DropdownLanguage from './DropdownLanguage'; import DropdownLanguage from './DropdownLanguage';
import UserAvatar from '@/components/UserAvatar'; import UserAvatar from '@/components/UserAvatar';
import { LocaleCodeType } from '@/types/locale'; import { LocaleCodeType } from '@/types/locale';
import Config from '@/config/config';
type NabbarMenuMessages = { type NabbarMenuMessages = {
projects: string; projects: string;
@@ -54,20 +55,25 @@ export default function HeaderNavbarMenu({ messages, locale }: Props) {
label: messages.projects, label: messages.projects,
isExternal: false, isExternal: false,
}, },
{
uid: 'docs',
href: 'https://kimatata.github.io/unittcms/docs/getstarted/selfhost',
label: messages.docs,
isExternal: true,
},
{
uid: 'roadmap',
href: 'https://kimatata.github.io/unittcms/docs/roadmap/',
label: messages.roadmap,
isExternal: true,
},
]; ];
if (Config.isDemoSite) {
commonLinks.push(
{
uid: 'docs',
href: 'https://kimatata.github.io/unittcms/docs/getstarted/selfhost',
label: messages.docs,
isExternal: true,
},
{
uid: 'roadmap',
href: 'https://kimatata.github.io/unittcms/docs/roadmap/',
label: messages.roadmap,
isExternal: true,
}
);
}
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
async function changeLocale(nextLocale: string) { async function changeLocale(nextLocale: string) {
@@ -94,7 +100,7 @@ export default function HeaderNavbarMenu({ messages, locale }: Props) {
<NavbarItem className="hidden md:block"> <NavbarItem className="hidden md:block">
<Chip size="sm" variant="flat"> <Chip size="sm" variant="flat">
<Link className="data-[active=true]:text-primary data-[active=true]:font-medium" href="/" locale={locale}> <Link className="data-[active=true]:text-primary data-[active=true]:font-medium" href="/" locale={locale}>
1.0.0-beta.12 1.0.0-beta.13
</Link> </Link>
</Chip> </Chip>
</NavbarItem> </NavbarItem>

View File

@@ -0,0 +1,106 @@
import { useTranslations } from 'next-intl';
import { Divider } from '@heroui/react';
import PaneMainTitle from './PaneMainTitle';
import PaneMainFeatures from './PaneMainFeatures';
import DemoImage from './DemoImage';
import { title, subtitle } from '@/components/primitives';
import { PageType } from '@/types/base';
import { LocaleCodeType } from '@/types/locale';
export default function LandingPage({ params }: PageType) {
const t = useTranslations('Index');
const demoImages = [
{
uid: 'project',
title: t('project_title'),
subTitle: t('project_subtitle'),
},
{
uid: 'case',
title: t('case_management_title'),
subTitle: t('case_management_subtitle'),
},
{
uid: 'run',
title: t('run_management_title'),
subTitle: t('run_management_subtitle'),
},
{
uid: 'member',
title: t('member_management_title'),
subTitle: t('member_management_subtitle'),
},
];
return (
<section className="mx-auto max-w-screen-xl my-12">
<div className="flex flex-wrap">
<div className="w-full md:w-7/12 order-last md:order-first p-4">
<PaneMainTitle locale={params.locale as LocaleCodeType} />
</div>
<div className="w-full md:w-5/12 p-4">
<div
style={{
position: 'relative',
textAlign: 'center',
}}
>
<span
style={{
fontSize: '10rem',
position: 'relative',
zIndex: 1,
}}
>
📋
</span>
<div
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
filter: 'blur(48px)',
zIndex: 0,
background: 'linear-gradient(to bottom, #ffecd2, #fcb69f)',
}}
></div>
</div>
</div>
</div>
<div
className="flex flex-wrap flex-col items-center"
style={{
marginTop: '6rem',
}}
>
<PaneMainFeatures />
</div>
<Divider className="my-12" />
<div className="flex flex-wrap lg:text-left text-center">
{demoImages.map((demoImage) => (
<div key={demoImage.uid} className="flex flex-wrap">
<div className="w-full lg:w-5/12 p-4">
<h2 className={title({ size: 'sm', color: 'pink' })}>{demoImage.title}</h2>
<h4 className={subtitle({ class: 'mt-4' })}>{demoImage.subTitle}</h4>
</div>
<div className="flex justify-center w-full lg:w-7/12 p-4">
<DemoImage imageName={demoImage.uid} altText={demoImage.title} />
</div>
</div>
))}
</div>
<Divider className="my-12" />
<div className="w-full text-center py-2">
<div>Copyright © 2024-present UnitTCMS</div>
</div>
</section>
);
}

View File

@@ -1,106 +1,13 @@
import { useTranslations } from 'next-intl'; import { redirect } from 'next/navigation'
import { Divider } from '@heroui/react';
import { title, subtitle } from '@/components/primitives';
import PaneMainTitle from './PaneMainTitle';
import PaneMainFeatures from './PaneMainFeatures';
import DemoImage from './DemoImage';
import { PageType } from '@/types/base'; import { PageType } from '@/types/base';
import { LocaleCodeType } from '@/types/locale'; import Config from '@/config/config';
import LandingPage from '@/src/app/[locale]/LandingPage';
export default function Home({ params }: PageType) { export default function Home({ params }: PageType) {
const t = useTranslations('Index');
const demoImages = [ if (!Config.isDemoSite) {
{ redirect(`/${params.locale}/projects`)
uid: 'project', }
title: t('project_title'),
subTitle: t('project_subtitle'),
},
{
uid: 'case',
title: t('case_management_title'),
subTitle: t('case_management_subtitle'),
},
{
uid: 'run',
title: t('run_management_title'),
subTitle: t('run_management_subtitle'),
},
{
uid: 'member',
title: t('member_management_title'),
subTitle: t('member_management_subtitle'),
},
];
return ( return <LandingPage params={params} />
<section className="mx-auto max-w-screen-xl my-12">
<div className="flex flex-wrap">
<div className="w-full md:w-7/12 order-last md:order-first p-4">
<PaneMainTitle locale={params.locale as LocaleCodeType} />
</div>
<div className="w-full md:w-5/12 p-4">
<div
style={{
position: 'relative',
textAlign: 'center',
}}
>
<span
style={{
fontSize: '10rem',
position: 'relative',
zIndex: 1,
}}
>
📋
</span>
<div
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
filter: 'blur(48px)',
zIndex: 0,
background: 'linear-gradient(to bottom, #ffecd2, #fcb69f)',
}}
></div>
</div>
</div>
</div>
<div
className="flex flex-wrap flex-col items-center"
style={{
marginTop: '6rem',
}}
>
<PaneMainFeatures />
</div>
<Divider className="my-12" />
<div className="flex flex-wrap lg:text-left text-center">
{demoImages.map((demoImage) => (
<div key={demoImage.uid} className="flex flex-wrap">
<div className="w-full lg:w-5/12 p-4">
<h2 className={title({ size: 'sm', color: 'pink' })}>{demoImage.title}</h2>
<h4 className={subtitle({ class: 'mt-4' })}>{demoImage.subTitle}</h4>
</div>
<div className="flex justify-center w-full lg:w-7/12 p-4">
<DemoImage imageName={demoImage.uid} altText={demoImage.title} />
</div>
</div>
))}
</div>
<Divider className="my-12" />
<div className="w-full text-center py-2">
<div>Copyright © 2024-present UnitTCMS</div>
</div>
</section>
);
} }