mirror of
https://github.com/meienberger/runtipi.git
synced 2024-10-03 23:28:33 +03:00
release: 3.4.0 (#1459)
* Release/3.3.1 (#1439) * chore: bump version to 3.3.1 * ci: run e2e when build-images is skipped * feat: view app logs in dashboard (#1445) * socket events for docker logs * auto-scroll with ref * add log terminal with auto scroll * remove console.logs * increase initial lines to 25 * remove more console logs * useSocketEmit * emit on disconect & hide tab if not running * change tab when not running & logs options * logs max lines * logs emit * remove console logs * refactor(logs-socket): consolidate & reduce state usage * useTranslations in logs tab * remove wrapLines from useEffect Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * order file & add spanish translations * chore: fix tsc issues --------- Co-authored-by: Jorge Montejo <jorgemon.lopez@gmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * autoforward ports in codespaces (#1447) * chore(deps-dev): bump @typescript-eslint/parser from 6.21.0 to 7.11.0 (#1443) Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.21.0 to 7.11.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.11.0/packages/parser) --- updated-dependencies: - dependency-name: "@typescript-eslint/parser" dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix: sentry config (#1448) * chore(deps): bump the minor-patch group across 1 directory with 17 updates (#1451) * chore(deps): bump the minor-patch group across 1 directory with 17 updates Bumps the minor-patch group with 17 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@sentry/nextjs](https://github.com/getsentry/sentry-javascript) | `8.4.0` | `8.7.0` | | [@tabler/icons-react](https://github.com/tabler/tabler-icons/tree/HEAD/packages/icons-react) | `3.4.0` | `3.5.0` | | [argon2](https://github.com/ranisalt/node-argon2) | `0.40.1` | `0.40.3` | | [bullmq](https://github.com/taskforcesh/bullmq) | `5.7.12` | `5.7.14` | | [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.51.4` | `7.51.5` | | [sass](https://github.com/sass/dart-sass) | `1.77.2` | `1.77.3` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `20.12.12` | `20.12.13` | | [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `7.10.0` | `7.11.0` | | [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `4.2.1` | `4.3.0` | | [eslint-plugin-jsonc](https://github.com/ota-meshi/eslint-plugin-jsonc) | `2.15.1` | `2.16.0` | | [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) | `7.34.1` | `7.34.2` | | [jsdom](https://github.com/jsdom/jsdom) | `24.0.0` | `24.1.0` | | [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) | `5.16.0` | `5.17.3` | | [@sentry/types](https://github.com/getsentry/sentry-javascript) | `8.4.0` | `8.7.0` | | [@sentry/node](https://github.com/getsentry/sentry-javascript) | `8.4.0` | `8.7.0` | | [hono](https://github.com/honojs/hono) | `4.3.11` | `4.4.0` | | [nodemon](https://github.com/remy/nodemon) | `3.1.0` | `3.1.2` | Updates `@sentry/nextjs` from 8.4.0 to 8.7.0 - [Release notes](https://github.com/getsentry/sentry-javascript/releases) - [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-javascript/compare/8.4.0...8.7.0) Updates `@tabler/icons-react` from 3.4.0 to 3.5.0 - [Release notes](https://github.com/tabler/tabler-icons/releases) - [Commits](https://github.com/tabler/tabler-icons/commits/v3.5.0/packages/icons-react) Updates `argon2` from 0.40.1 to 0.40.3 - [Release notes](https://github.com/ranisalt/node-argon2/releases) - [Commits](https://github.com/ranisalt/node-argon2/commits) Updates `bullmq` from 5.7.12 to 5.7.14 - [Release notes](https://github.com/taskforcesh/bullmq/releases) - [Commits](https://github.com/taskforcesh/bullmq/compare/v5.7.12...v5.7.14) Updates `react-hook-form` from 7.51.4 to 7.51.5 - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.51.4...v7.51.5) Updates `sass` from 1.77.2 to 1.77.3 - [Release notes](https://github.com/sass/dart-sass/releases) - [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md) - [Commits](https://github.com/sass/dart-sass/compare/1.77.2...1.77.3) Updates `@types/node` from 20.12.12 to 20.12.13 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@typescript-eslint/eslint-plugin` from 7.10.0 to 7.11.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.11.0/packages/eslint-plugin) Updates `@vitejs/plugin-react` from 4.2.1 to 4.3.0 - [Release notes](https://github.com/vitejs/vite-plugin-react/releases) - [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-react/commits/v4.3.0/packages/plugin-react) Updates `eslint-plugin-jsonc` from 2.15.1 to 2.16.0 - [Release notes](https://github.com/ota-meshi/eslint-plugin-jsonc/releases) - [Changelog](https://github.com/ota-meshi/eslint-plugin-jsonc/blob/master/CHANGELOG.md) - [Commits](https://github.com/ota-meshi/eslint-plugin-jsonc/compare/v2.15.1...v2.16.0) Updates `eslint-plugin-react` from 7.34.1 to 7.34.2 - [Release notes](https://github.com/jsx-eslint/eslint-plugin-react/releases) - [Changelog](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/CHANGELOG.md) - [Commits](https://github.com/jsx-eslint/eslint-plugin-react/compare/v7.34.1...v7.34.2) Updates `jsdom` from 24.0.0 to 24.1.0 - [Release notes](https://github.com/jsdom/jsdom/releases) - [Changelog](https://github.com/jsdom/jsdom/blob/main/Changelog.md) - [Commits](https://github.com/jsdom/jsdom/compare/24.0.0...24.1.0) Updates `knip` from 5.16.0 to 5.17.3 - [Release notes](https://github.com/webpro-nl/knip/releases) - [Changelog](https://github.com/webpro-nl/knip/blob/main/packages/knip/.release-it.json) - [Commits](https://github.com/webpro-nl/knip/commits/5.17.3/packages/knip) Updates `@sentry/types` from 8.4.0 to 8.7.0 - [Release notes](https://github.com/getsentry/sentry-javascript/releases) - [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-javascript/compare/8.4.0...8.7.0) Updates `@sentry/node` from 8.4.0 to 8.7.0 - [Release notes](https://github.com/getsentry/sentry-javascript/releases) - [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-javascript/compare/8.4.0...8.7.0) Updates `hono` from 4.3.11 to 4.4.0 - [Release notes](https://github.com/honojs/hono/releases) - [Commits](https://github.com/honojs/hono/compare/v4.3.11...v4.4.0) Updates `nodemon` from 3.1.0 to 3.1.2 - [Release notes](https://github.com/remy/nodemon/releases) - [Commits](https://github.com/remy/nodemon/compare/v3.1.0...v3.1.2) --- updated-dependencies: - dependency-name: "@sentry/nextjs" dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@tabler/icons-react" dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: argon2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: bullmq dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: react-hook-form dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: sass dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: "@types/node" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: "@typescript-eslint/eslint-plugin" dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@vitejs/plugin-react" dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: eslint-plugin-jsonc dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: eslint-plugin-react dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: jsdom dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: knip dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@sentry/types" dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@sentry/node" dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: hono dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: nodemon dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> * chore: fix vite plugin typings --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Nicolas Meienberger <github@thisprops.com> * Feat/tipi logs terminal (#1450) * refactor: extract logs terminal ui in its own component * feat: runtipi logs settings * fix: runtipi dashboard logs project name * ci: use nightly version for e2e * feat(docker-tamplate): include addPorts and readOnly in volumes (#1456) * chore(deps): bump the minor-patch group with 6 updates (#1454) * chore: bump version to 3.4.0 * chore: implement bot readability feedbacks * refactor(logs): only scroll when the last log id changes --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Jorge Montejo <jorgemon.lopez@gmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Stavros <steveiliop56@gmail.com>
This commit is contained in:
parent
44e9334120
commit
9eb6301ada
@ -11,5 +11,17 @@
|
||||
"ms-vscode.vscode-typescript-next",
|
||||
"waderyan.gitblame"
|
||||
],
|
||||
"postCreateCommand": "./.devcontainer/postCreateCommand.sh"
|
||||
}
|
||||
"postCreateCommand": "./.devcontainer/postCreateCommand.sh",
|
||||
"forwardPorts": [
|
||||
80,
|
||||
3000
|
||||
],
|
||||
"portsAttributes": {
|
||||
"3000": {
|
||||
"label": "Runtipi"
|
||||
},
|
||||
"80": {
|
||||
"label": "WebSocket"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
7
.github/workflows/nightly-release.yml
vendored
7
.github/workflows/nightly-release.yml
vendored
@ -104,3 +104,10 @@ jobs:
|
||||
tag: nightly
|
||||
rm: true
|
||||
files: cli/runtipi-cli-*
|
||||
|
||||
e2e-tests:
|
||||
needs: [update-release]
|
||||
uses: './.github/workflows/e2e.yml'
|
||||
secrets: inherit
|
||||
with:
|
||||
version: nightly
|
||||
|
@ -1,3 +1,17 @@
|
||||
packages
|
||||
coverage
|
||||
.github
|
||||
# Next files
|
||||
.next
|
||||
|
||||
# Node modules
|
||||
node_modules
|
||||
packages/shared/node_modules
|
||||
packages/worker/node_modules
|
||||
|
||||
# Tipi data
|
||||
app-data
|
||||
apps
|
||||
logs
|
||||
media
|
||||
repos
|
||||
state
|
||||
traefik
|
||||
user-config
|
||||
|
@ -11,7 +11,7 @@ ARG TIPI_VERSION
|
||||
ARG LOCAL
|
||||
|
||||
ENV SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN}
|
||||
ENV TIPI_VERSION=${TIPI_VERSION}
|
||||
ENV SENTRY_RELEASE=${TIPI_VERSION}
|
||||
ENV LOCAL=${LOCAL}
|
||||
|
||||
RUN npm install pnpm -g
|
||||
@ -45,8 +45,6 @@ COPY ./tests ./tests
|
||||
|
||||
# Sentry
|
||||
COPY ./sentry.client.config.ts ./sentry.client.config.ts
|
||||
COPY ./sentry.edge.config.ts ./sentry.edge.config.ts
|
||||
COPY ./sentry.server.config.ts ./sentry.server.config.ts
|
||||
|
||||
RUN pnpm build
|
||||
|
||||
@ -88,7 +86,7 @@ COPY ./packages/worker/package.json ./packages/worker/package.json
|
||||
COPY ./packages/worker/assets ./packages/worker/assets
|
||||
|
||||
# Print TIPI_VERSION to the console
|
||||
RUN echo "TIPI_VERSION: ${TIPI_VERSION}"
|
||||
RUN echo "TIPI_VERSION: ${SENTRY_RELEASE}"
|
||||
|
||||
RUN pnpm -r --filter @runtipi/worker build
|
||||
|
||||
|
@ -3,6 +3,9 @@ ARG ALPINE_VERSION="3.18"
|
||||
|
||||
FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION}
|
||||
|
||||
ENV LOCAL=true
|
||||
ENV SENTRY_RELEASE=development
|
||||
|
||||
RUN apk add --no-cache python3 make g++ curl git
|
||||
RUN npm install pnpm pm2 -g
|
||||
|
||||
@ -43,8 +46,6 @@ COPY ./next.config.mjs ./next.config.mjs
|
||||
|
||||
# Sentry
|
||||
COPY ./sentry.client.config.ts ./sentry.client.config.ts
|
||||
COPY ./sentry.edge.config.ts ./sentry.edge.config.ts
|
||||
COPY ./sentry.server.config.ts ./sentry.server.config.ts
|
||||
|
||||
COPY ./start.dev.sh ./start.sh
|
||||
|
||||
|
@ -82,6 +82,7 @@ services:
|
||||
- ${RUNTIPI_APP_DATA_PATH:-.}/app-data:/app-data
|
||||
# Static
|
||||
- ./.env:/data/.env
|
||||
- ./docker-compose.dev.yml:/data/docker-compose.yml
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- /proc:/host/proc:ro
|
||||
networks:
|
||||
@ -89,12 +90,10 @@ services:
|
||||
ports:
|
||||
- 3000:3000
|
||||
environment:
|
||||
LOCAL: true
|
||||
NODE_ENV: development
|
||||
TIPI_VERSION: development
|
||||
WORKER_APP_DIR: /app/packages/worker
|
||||
DASHBOARD_APP_DIR: /app
|
||||
NEXT_PUBLIC_TIPI_VERSION: development
|
||||
LOCAL: true
|
||||
env_file:
|
||||
- .env
|
||||
labels:
|
||||
|
@ -61,8 +61,8 @@ services:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
TIPI_VERSION: development
|
||||
LOCAL: true
|
||||
TIPI_VERSION: development
|
||||
container_name: runtipi
|
||||
healthcheck:
|
||||
test: ['CMD', 'curl', '-f', 'http://localhost:5000/worker-api/healthcheck']
|
||||
@ -89,13 +89,12 @@ services:
|
||||
- ${RUNTIPI_APP_DATA_PATH:-.}/app-data:/app-data
|
||||
# Static
|
||||
- ./.env:/data/.env
|
||||
- ./docker-compose.prod.yml:/data/docker-compose.yml
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- /proc:/host/proc
|
||||
- /proc:/host/proc:ro
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
TIPI_VERSION: development
|
||||
NEXT_PUBLIC_TIPI_VERSION: development
|
||||
LOCAL: true
|
||||
NODE_ENV: production
|
||||
networks:
|
||||
- tipi_main_network
|
||||
ports:
|
||||
|
@ -21,23 +21,16 @@ const nextConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
export default !process.env.LOCAL === 'true'
|
||||
? withSentryConfig(
|
||||
nextConfig,
|
||||
{
|
||||
// https://github.com/getsentry/sentry-webpack-plugin#options
|
||||
silent: false,
|
||||
org: 'runtipi',
|
||||
project: 'runtipi-dashboard',
|
||||
release: process.env.TIPI_VERSION,
|
||||
},
|
||||
{
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
|
||||
widenClientFileUpload: true,
|
||||
transpileClientSDK: false,
|
||||
tunnelRoute: '/errors',
|
||||
hideSourceMaps: false,
|
||||
disableLogger: true,
|
||||
},
|
||||
)
|
||||
: nextConfig;
|
||||
const sentryConfig = {
|
||||
silent: false,
|
||||
org: 'runtipi',
|
||||
project: 'runtipi-dashboard',
|
||||
widenClientFileUpload: true,
|
||||
tunnelRoute: '/errors',
|
||||
hideSourceMaps: false,
|
||||
disableLogger: false,
|
||||
};
|
||||
|
||||
const config = process.env.LOCAL !== 'true' ? withSentryConfig(nextConfig, sentryConfig) : nextConfig;
|
||||
|
||||
export default config;
|
||||
|
36
package.json
36
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "runtipi",
|
||||
"version": "3.3.2",
|
||||
"version": "3.4.0",
|
||||
"description": "A homeserver for everyone",
|
||||
"scripts": {
|
||||
"clean-containers": "docker rm -f $(docker ps -a -q)",
|
||||
@ -15,8 +15,8 @@
|
||||
"build": "next build --experimental-build-mode compile",
|
||||
"start": "NODE_ENV=production node server.js",
|
||||
"start:dev-container": "./.devcontainer/filewatcher.sh && npm run start:dev",
|
||||
"start:dev": "docker compose -f docker-compose.dev.yml up --build",
|
||||
"start:prod": "docker compose --env-file ./.env -f docker-compose.prod.yml up --build",
|
||||
"start:dev": "docker compose --project-name runtipi -f docker-compose.dev.yml up --build",
|
||||
"start:prod": "docker compose --env-file ./.env --project-name runtipi -f docker-compose.prod.yml up --build",
|
||||
"start:pg": "docker run --name test-db -p 5433:5432 -d --rm -e POSTGRES_PASSWORD=postgres postgres:14",
|
||||
"version": "echo $npm_package_version",
|
||||
"tsc": "tsc",
|
||||
@ -38,13 +38,13 @@
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@runtipi/postgres-migrations": "^5.3.0",
|
||||
"@runtipi/shared": "workspace:^",
|
||||
"@sentry/nextjs": "^8.4.0",
|
||||
"@sentry/nextjs": "^8.7.0",
|
||||
"@tabler/core": "1.0.0-beta20",
|
||||
"@tabler/icons-react": "^3.2.0",
|
||||
"argon2": "^0.40.1",
|
||||
"bullmq": "^5.7.12",
|
||||
"@tabler/icons-react": "^3.5.0",
|
||||
"argon2": "^0.40.3",
|
||||
"bullmq": "^5.7.14",
|
||||
"clsx": "^2.1.0",
|
||||
"drizzle-orm": "^0.30.9",
|
||||
"drizzle-orm": "^0.31.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"geist": "^1.3.0",
|
||||
"ipaddr.js": "^2.2.0",
|
||||
@ -60,7 +60,7 @@
|
||||
"qrcode.react": "^3.1.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-hook-form": "^7.51.4",
|
||||
"react-hook-form": "^7.51.5",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-query": "^3.39.3",
|
||||
@ -70,7 +70,7 @@
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"sass": "^1.77.1",
|
||||
"sass": "^1.77.4",
|
||||
"semver": "^7.6.2",
|
||||
"sharp": "0.33.4",
|
||||
"socket.io-client": "^4.7.5",
|
||||
@ -94,16 +94,16 @@
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/lodash.merge": "^4.6.9",
|
||||
"@types/node": "20.12.12",
|
||||
"@types/node": "20.13.0",
|
||||
"@types/pg": "^8.11.6",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@types/validator": "^13.11.10",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "^7.11.0",
|
||||
"@typescript-eslint/parser": "^7.11.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"@vitest/coverage-v8": "^1.5.0",
|
||||
"@vitest/ui": "^1.6.0",
|
||||
"dotenv-cli": "^7.4.1",
|
||||
@ -113,15 +113,15 @@
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jest": "^28.5.0",
|
||||
"eslint-plugin-jest-dom": "^5.4.0",
|
||||
"eslint-plugin-jsonc": "^2.15.1",
|
||||
"eslint-plugin-jsonc": "^2.16.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"eslint-plugin-react": "^7.34.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-testing-library": "^6.2.2",
|
||||
"jest": "^29.7.0",
|
||||
"jsdom": "^24.0.0",
|
||||
"jsdom": "^24.1.0",
|
||||
"jsonc-eslint-parser": "^2.4.0",
|
||||
"knip": "^5.9.4",
|
||||
"knip": "^5.17.3",
|
||||
"memfs": "^4.8.2",
|
||||
"msw": "^2.3.0",
|
||||
"next-router-mock": "^0.9.13",
|
||||
|
@ -38,10 +38,10 @@
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sentry/types": "^8.4.0",
|
||||
"@sentry/types": "^8.7.0",
|
||||
"@types/lodash.clonedeep": "^4.5.9",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.11.0",
|
||||
"@typescript-eslint/parser": "^7.11.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"prettier": "^3.2.5"
|
||||
|
@ -28,10 +28,33 @@ export const socketEventSchema = z.union([
|
||||
}),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('dummy'),
|
||||
event: z.literal('dummy_event'),
|
||||
type: z.literal('app-logs-init'),
|
||||
event: z.literal('initLogs'),
|
||||
data: z.object({
|
||||
dummy: z.string(),
|
||||
appId: z.string(),
|
||||
maxLines: z.number().optional(),
|
||||
}),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('app-logs'),
|
||||
event: z.union([z.literal('newLogs'), z.literal('stopLogs')]),
|
||||
data: z.object({
|
||||
appId: z.string(),
|
||||
lines: z.array(z.string()).optional(),
|
||||
}),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('runtipi-logs-init'),
|
||||
event: z.literal('initLogs'),
|
||||
data: z.object({
|
||||
maxLines: z.number().optional(),
|
||||
}),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('runtipi-logs'),
|
||||
event: z.union([z.literal('newLogs'), z.literal('stopLogs')]),
|
||||
data: z.object({
|
||||
lines: z.array(z.string()).optional(),
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
@ -20,15 +20,15 @@
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@sentry/esbuild-plugin": "^2.17.0",
|
||||
"@types/web-push": "^3.6.3",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.11.0",
|
||||
"@typescript-eslint/parser": "^7.11.0",
|
||||
"dotenv-cli": "^7.4.2",
|
||||
"esbuild": "^0.19.4",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"knip": "^5.15.1",
|
||||
"knip": "^5.17.3",
|
||||
"memfs": "^4.8.2",
|
||||
"nodemon": "^3.1.0",
|
||||
"nodemon": "^3.1.2",
|
||||
"prettier": "^3.2.5",
|
||||
"tsx": "^4.10.2",
|
||||
"typescript": "^5.4.5",
|
||||
@ -36,18 +36,18 @@
|
||||
"vitest": "^1.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.11.0",
|
||||
"@hono/node-server": "^1.11.2",
|
||||
"@runtipi/postgres-migrations": "^5.3.0",
|
||||
"@runtipi/shared": "workspace:^",
|
||||
"@sentry/integrations": "^7.114.0",
|
||||
"@sentry/node": "^8.4.0",
|
||||
"bullmq": "^5.7.12",
|
||||
"@sentry/node": "^8.7.0",
|
||||
"bullmq": "^5.7.14",
|
||||
"dotenv": "^16.4.5",
|
||||
"hono": "^4.3.11",
|
||||
"hono": "^4.4.2",
|
||||
"ioredis": "^5.4.1",
|
||||
"pg": "^8.11.5",
|
||||
"socket.io": "^4.7.5",
|
||||
"systeminformation": "^5.22.9",
|
||||
"systeminformation": "^5.22.10",
|
||||
"web-push": "^3.6.7",
|
||||
"yaml": "^2.4.1",
|
||||
"zod": "^3.23.8"
|
||||
|
@ -71,12 +71,23 @@ const serviceSchema = z
|
||||
name: z.string(),
|
||||
internalPort: z.number(),
|
||||
isMain: z.boolean().optional(),
|
||||
addPorts: z
|
||||
.array(
|
||||
z.object({
|
||||
containerPort: z.number(),
|
||||
hostPort: z.number(),
|
||||
udp: z.boolean(),
|
||||
tcp: z.boolean(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
command: z.string().optional(),
|
||||
volumes: z
|
||||
.array(
|
||||
z.object({
|
||||
hostPath: z.string(),
|
||||
containerPath: z.string(),
|
||||
readOnly: z.boolean().optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
@ -104,13 +115,31 @@ const serviceSchema = z
|
||||
command: data.command,
|
||||
};
|
||||
|
||||
let ports: string[] = [];
|
||||
|
||||
if (data.isMain && data.openPort) {
|
||||
base.ports = [`\${APP_PORT}:${data.internalPort}`];
|
||||
ports = ports.concat([`\${APP_PORT}:${data.internalPort}`]);
|
||||
}
|
||||
|
||||
if (data.addPorts?.length) {
|
||||
for (const port of data.addPorts) {
|
||||
if (port.tcp) {
|
||||
ports = ports.concat([`${port.hostPort}:${port.containerPort}/tcp`]);
|
||||
}
|
||||
if (port.udp) {
|
||||
ports = ports.concat([`${port.hostPort}:${port.containerPort}/udp`]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ports.length) {
|
||||
base.ports = ports;
|
||||
}
|
||||
|
||||
if (data.volumes?.length) {
|
||||
base.volumes = data.volumes.map(
|
||||
({ hostPath, containerPath }) => `${hostPath}:${containerPath}`,
|
||||
({ hostPath, containerPath, readOnly }) =>
|
||||
`${hostPath}:${containerPath}${readOnly ? ':ro' : ''}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,27 +1,14 @@
|
||||
import path from 'path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { execAsync, pathExists } from '@runtipi/shared/node';
|
||||
import { sanitizePath } from '@runtipi/shared';
|
||||
import { SocketEvent, sanitizePath, socketEventSchema } from '@runtipi/shared';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { getEnv } from '@/lib/environment';
|
||||
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
|
||||
import { Socket } from 'socket.io';
|
||||
import { SocketManager } from '@/lib/socket/SocketManager';
|
||||
|
||||
const composeUp = async (args: string[]) => {
|
||||
logger.info(`Running docker compose with args ${args.join(' ')}`);
|
||||
const { stdout, stderr } = await execAsync(`docker-compose ${args.join(' ')}`);
|
||||
|
||||
if (stderr && stderr.includes('Command failed:')) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
|
||||
return { stdout, stderr };
|
||||
};
|
||||
|
||||
/**
|
||||
* Helpers to execute docker compose commands
|
||||
* @param {string} appId - App name
|
||||
* @param {string} command - Command to execute
|
||||
*/
|
||||
export const compose = async (appId: string, command: string) => {
|
||||
const getBaseComposeArgsApp = async (appId: string) => {
|
||||
const { arch, appsRepoId } = getEnv();
|
||||
const appDataDirPath = path.join(APP_DATA_DIR, sanitizePath(appId));
|
||||
const appDirPath = path.join(DATA_DIR, 'apps', sanitizePath(appId));
|
||||
@ -42,16 +29,163 @@ export const compose = async (appId: string, command: string) => {
|
||||
}
|
||||
args.push(`-f ${composeFile}`);
|
||||
|
||||
const commonComposeFile = path.join(DATA_DIR, 'repos', sanitizePath(appsRepoId), 'apps', 'docker-compose.common.yml');
|
||||
const commonComposeFile = path.join(
|
||||
DATA_DIR,
|
||||
'repos',
|
||||
sanitizePath(appsRepoId),
|
||||
'apps',
|
||||
'docker-compose.common.yml',
|
||||
);
|
||||
args.push(`-f ${commonComposeFile}`);
|
||||
|
||||
// User defined overrides
|
||||
const userComposeFile = path.join(DATA_DIR, 'user-config', sanitizePath(appId), 'docker-compose.yml');
|
||||
const userComposeFile = path.join(
|
||||
DATA_DIR,
|
||||
'user-config',
|
||||
sanitizePath(appId),
|
||||
'docker-compose.yml',
|
||||
);
|
||||
if (await pathExists(userComposeFile)) {
|
||||
args.push(`--file ${userComposeFile}`);
|
||||
}
|
||||
|
||||
return args;
|
||||
};
|
||||
|
||||
const getBaseComposeArgsTipi = async () => {
|
||||
const args: string[] = [`--env-file ${path.join(DATA_DIR, '.env')}`];
|
||||
|
||||
args.push(`--project-name runtipi`);
|
||||
|
||||
const composeFile = path.join(DATA_DIR, 'docker-compose.yml');
|
||||
args.push(`-f ${composeFile}`);
|
||||
|
||||
// User defined overrides
|
||||
const userComposeFile = path.join(DATA_DIR, 'user-config', 'tipi-compose.yml');
|
||||
if (await pathExists(userComposeFile)) {
|
||||
args.push(`--file ${userComposeFile}`);
|
||||
}
|
||||
|
||||
return args;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helpers to execute docker compose commands
|
||||
* @param {string} appId - App name
|
||||
* @param {string} command - Command to execute
|
||||
*/
|
||||
export const compose = async (appId: string, command: string) => {
|
||||
const args = await getBaseComposeArgsApp(appId);
|
||||
args.push(command);
|
||||
|
||||
return composeUp(args);
|
||||
logger.info(`Running docker compose with args ${args.join(' ')}`);
|
||||
const { stdout, stderr } = await execAsync(`docker-compose ${args.join(' ')}`);
|
||||
|
||||
if (stderr && stderr.includes('Command failed:')) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
|
||||
return { stdout, stderr };
|
||||
};
|
||||
|
||||
export const handleViewRuntipiLogsEvent = async (socket: Socket, event: SocketEvent) => {
|
||||
const { success, data } = socketEventSchema.safeParse(event);
|
||||
|
||||
if (!success) {
|
||||
logger.error('Invalid viewLogs event data:', event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.type !== 'runtipi-logs-init') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { maxLines } = data.data;
|
||||
|
||||
const args = await getBaseComposeArgsTipi();
|
||||
|
||||
args.push(`logs --follow -n ${maxLines || 25}`);
|
||||
|
||||
const logsCommand = `docker-compose ${args.join(' ')}`;
|
||||
|
||||
const logs = spawn('sh', ['-c', logsCommand]);
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
logs.kill('SIGINT');
|
||||
});
|
||||
|
||||
socket.on('runtipi-logs', (data) => {
|
||||
if (data.event === 'stopLogs') {
|
||||
logs.kill('SIGINT');
|
||||
}
|
||||
});
|
||||
|
||||
logs.on('error', (error) => {
|
||||
logger.error('Error running logs command: ', error);
|
||||
logs.kill('SIGINT');
|
||||
});
|
||||
|
||||
logs.stdout.on('data', async (data) => {
|
||||
const lines = data
|
||||
.toString()
|
||||
.split(/(?:\r\n|\r|\n)/g)
|
||||
.filter(Boolean);
|
||||
|
||||
await SocketManager.emit({
|
||||
type: 'runtipi-logs',
|
||||
event: 'newLogs',
|
||||
data: { lines },
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const handleViewAppLogsEvent = async (socket: Socket, event: SocketEvent) => {
|
||||
const parsedEvent = socketEventSchema.safeParse(event);
|
||||
|
||||
if (!parsedEvent.success) {
|
||||
logger.error('Invalid viewLogs event data:', event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedEvent.data.type !== 'app-logs-init') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { appId, maxLines } = parsedEvent.data.data;
|
||||
|
||||
const args = await getBaseComposeArgsApp(appId);
|
||||
args.push(`logs --follow -n ${maxLines || 25}`);
|
||||
|
||||
const logsCommand = `docker-compose ${args.join(' ')}`;
|
||||
|
||||
const logs = spawn('sh', ['-c', logsCommand]);
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
logs.kill('SIGINT');
|
||||
});
|
||||
|
||||
socket.on('app-logs', (data) => {
|
||||
if (data.event === 'stopLogs') {
|
||||
console.log('Stopping logs');
|
||||
logs.kill('SIGINT');
|
||||
}
|
||||
});
|
||||
|
||||
logs.on('error', (error) => {
|
||||
logger.error('Error running logs command: ', error);
|
||||
logs.kill('SIGINT');
|
||||
});
|
||||
|
||||
logs.stdout.on('data', async (data) => {
|
||||
const lines = data
|
||||
.toString()
|
||||
.split(/(?:\r\n|\r|\n)/g)
|
||||
.filter(Boolean);
|
||||
|
||||
await SocketManager.emit({
|
||||
type: 'app-logs',
|
||||
event: 'newLogs',
|
||||
data: { lines, appId },
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { SocketEvent } from '@runtipi/shared';
|
||||
import { Server } from 'socket.io';
|
||||
import { logger } from '../logger';
|
||||
import { handleViewAppLogsEvent, handleViewRuntipiLogsEvent } from '../docker';
|
||||
|
||||
class SocketManager {
|
||||
private io: Server | null = null;
|
||||
@ -8,7 +9,9 @@ class SocketManager {
|
||||
init() {
|
||||
const io = new Server(5001, { cors: { origin: '*' }, path: '/worker/socket.io' });
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
io.on('connection', async (socket) => {
|
||||
socket.on('app-logs-init', (event) => handleViewAppLogsEvent(socket, event));
|
||||
socket.on('runtipi-logs-init', (event) => handleViewRuntipiLogsEvent(socket, event));
|
||||
socket.on('disconnect', () => {});
|
||||
});
|
||||
|
||||
|
1048
pnpm-lock.yaml
1048
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -21,13 +21,10 @@ const { allowErrorMonitoring } = getClientConfig();
|
||||
|
||||
if (allowErrorMonitoring && process.env.NODE_ENV === 'production' && process.env.LOCAL !== 'true') {
|
||||
Sentry.init({
|
||||
release: process.env.NEXT_PUBLIC_TIPI_VERSION,
|
||||
environment: process.env.NODE_ENV,
|
||||
dsn: 'https://7a73d72f886948478b55621e7b92c3c7@o4504242900238336.ingest.sentry.io/4504826587971584',
|
||||
beforeSend: cleanseErrorData,
|
||||
integrations: [Sentry.extraErrorDataIntegration()],
|
||||
initialScope: {
|
||||
tags: { version: process.env.TIPI_VERSION },
|
||||
},
|
||||
tracesSampleRate: 1.0,
|
||||
});
|
||||
}
|
||||
|
@ -1,23 +0,0 @@
|
||||
// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
|
||||
// The config you add here will be used whenever one of the edge features is loaded.
|
||||
// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import { TipiConfig } from '@/server/core/TipiConfig';
|
||||
import { cleanseErrorData } from '@runtipi/shared';
|
||||
|
||||
const { version, allowErrorMonitoring, NODE_ENV } = TipiConfig.getConfig();
|
||||
|
||||
if (allowErrorMonitoring && NODE_ENV === 'production' && process.env.LOCAL !== 'true') {
|
||||
Sentry.init({
|
||||
release: version,
|
||||
environment: NODE_ENV,
|
||||
dsn: 'https://7a73d72f886948478b55621e7b92c3c7@o4504242900238336.ingest.sentry.io/4504826587971584',
|
||||
beforeSend: cleanseErrorData,
|
||||
integrations: [Sentry.extraErrorDataIntegration()],
|
||||
initialScope: {
|
||||
tags: { version },
|
||||
},
|
||||
});
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
// This file configures the initialization of Sentry on the server.
|
||||
// The config you add here will be used whenever the server handles a request.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import { TipiConfig } from '@/server/core/TipiConfig';
|
||||
import { cleanseErrorData } from '@runtipi/shared';
|
||||
|
||||
const { version, allowErrorMonitoring, NODE_ENV } = TipiConfig.getConfig();
|
||||
|
||||
if (allowErrorMonitoring && NODE_ENV === 'production' && process.env.LOCAL !== 'true') {
|
||||
Sentry.init({
|
||||
release: version,
|
||||
environment: NODE_ENV,
|
||||
dsn: 'https://7a73d72f886948478b55621e7b92c3c7@o4504242900238336.ingest.sentry.io/4504826587971584',
|
||||
beforeSend: cleanseErrorData,
|
||||
integrations: [Sentry.extraErrorDataIntegration()],
|
||||
initialScope: {
|
||||
tags: { version },
|
||||
},
|
||||
});
|
||||
}
|
@ -236,7 +236,7 @@ export const AppDetailsContainer: React.FC<AppDetailsContainerProps> = ({ app, l
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<AppDetailsTabs info={app.info} />
|
||||
<AppDetailsTabs info={app.info} status={optimisticStatus}/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -5,12 +5,15 @@ import { useTranslations } from 'next-intl';
|
||||
import { AppInfo } from '@runtipi/shared';
|
||||
import { Markdown } from '@/components/Markdown';
|
||||
import { DataGrid, DataGridItem } from '@/components/ui/DataGrid';
|
||||
import { AppStatus as AppStatusEnum } from '@/server/db/schema';
|
||||
import { AppLogs } from './AppLogs';
|
||||
|
||||
interface IProps {
|
||||
info: AppInfo;
|
||||
status: AppStatusEnum;
|
||||
}
|
||||
|
||||
export const AppDetailsTabs: React.FC<IProps> = ({ info }) => {
|
||||
export const AppDetailsTabs: React.FC<IProps> = ({ info, status }) => {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
@ -18,6 +21,9 @@ export const AppDetailsTabs: React.FC<IProps> = ({ info }) => {
|
||||
<TabsList>
|
||||
<TabsTrigger value="description">{t('APP_DETAILS_DESCRIPTION')}</TabsTrigger>
|
||||
<TabsTrigger value="info">{t('APP_DETAILS_BASE_INFO')}</TabsTrigger>
|
||||
<TabsTrigger value="logs" disabled={status === 'missing'}>
|
||||
{t('APP_LOGS_TAB_TITLE')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="description">
|
||||
{info.deprecated && (
|
||||
@ -74,6 +80,7 @@ export const AppDetailsTabs: React.FC<IProps> = ({ info }) => {
|
||||
)}
|
||||
</DataGrid>
|
||||
</TabsContent>
|
||||
<TabsContent value="logs">{status === 'running' && <AppLogs appId={info.id} />}</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useSocket } from '@/lib/socket/useSocket';
|
||||
import { LogsTerminal } from 'src/app/components/LogsTerminal/LogsTerminal';
|
||||
|
||||
export const AppLogs = ({ appId }: { appId: string }) => {
|
||||
let nextId = 0;
|
||||
const [logs, setLogs] = useState<{ id: number; text: string }[]>([]);
|
||||
const [maxLines, setMaxLines] = useState<number>(300);
|
||||
|
||||
useSocket({
|
||||
selector: { type: 'app-logs', event: 'newLogs', data: { property: 'appId', value: appId } },
|
||||
onCleanup: () => setLogs([]),
|
||||
emitOnConnect: { type: 'app-logs-init', event: 'initLogs', data: { appId, maxLines } },
|
||||
emitOnDisconnect: { type: 'app-logs', event: 'stopLogs', data: { appId } },
|
||||
onEvent: (_, data) => {
|
||||
setLogs((prevLogs) => {
|
||||
if (!data.lines) {
|
||||
return prevLogs;
|
||||
}
|
||||
const newLogs = [...prevLogs, ...data.lines.map((line) => ({ id: nextId++, text: line.trim() }))];
|
||||
if (newLogs.length > maxLines) {
|
||||
return newLogs.slice(newLogs.length - maxLines);
|
||||
}
|
||||
return newLogs;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const updateMaxLines = (lines: number) => {
|
||||
const linesToKeep = Math.max(1, lines);
|
||||
setMaxLines(linesToKeep);
|
||||
setLogs((currentLogs) => currentLogs.slice(currentLogs.length - linesToKeep));
|
||||
};
|
||||
|
||||
return <LogsTerminal logs={logs} maxLines={maxLines} onMaxLinesChange={updateMaxLines} />;
|
||||
};
|
@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useSocket } from '@/lib/socket/useSocket';
|
||||
import { LogsTerminal } from 'src/app/components/LogsTerminal/LogsTerminal';
|
||||
|
||||
export const LogsContainer = () => {
|
||||
let nextId = 0;
|
||||
const [logs, setLogs] = useState<{ id: number; text: string }[]>([]);
|
||||
const [maxLines, setMaxLines] = useState<number>(300);
|
||||
|
||||
useSocket({
|
||||
selector: { type: 'runtipi-logs', event: 'newLogs' },
|
||||
onCleanup: () => setLogs([]),
|
||||
emitOnConnect: { type: 'runtipi-logs-init', event: 'initLogs', data: { maxLines } },
|
||||
emitOnDisconnect: { type: 'runtipi-logs', event: 'stopLogs', data: {} },
|
||||
onEvent: (_, data) => {
|
||||
setLogs((prevLogs) => {
|
||||
if (!data.lines) {
|
||||
return prevLogs;
|
||||
}
|
||||
const newLogs = [...prevLogs, ...data.lines.map((line) => ({ id: nextId++, text: line.trim() }))];
|
||||
if (newLogs.length > maxLines) {
|
||||
return newLogs.slice(newLogs.length - maxLines);
|
||||
}
|
||||
return newLogs;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const updateMaxLines = (lines: number) => {
|
||||
const linesToKeep = Math.max(1, lines);
|
||||
setMaxLines(linesToKeep);
|
||||
setLogs(logs.slice(logs.length - linesToKeep));
|
||||
};
|
||||
|
||||
return <LogsTerminal logs={logs} maxLines={maxLines} onMaxLinesChange={updateMaxLines} />;
|
||||
};
|
@ -0,0 +1 @@
|
||||
export { LogsContainer } from './LogsContainer';
|
@ -24,6 +24,9 @@ export const SettingsTabTriggers = () => {
|
||||
<TabsTrigger onClick={() => handleTabChange('security')} value="security">
|
||||
{t('SETTINGS_SECURITY_TAB_TITLE')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger onClick={() => handleTabChange('logs')} value="logs">
|
||||
{t('SETTINGS_LOGS_TAB_TITLE')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
);
|
||||
};
|
||||
|
@ -10,6 +10,7 @@ import { SettingsTabTriggers } from './components/SettingsTabTriggers';
|
||||
import { GeneralActions } from './components/GeneralActions';
|
||||
import { SettingsContainer } from './components/SettingsContainer';
|
||||
import { SecurityContainer } from './components/SecurityContainer';
|
||||
import { LogsContainer } from './components/LogsContainer';
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const translator = await getTranslatorFromCookie();
|
||||
@ -40,6 +41,9 @@ export default async function SettingsPage({ searchParams }: { searchParams: { t
|
||||
<TabsContent value="security">
|
||||
<SecurityContainer totpEnabled={Boolean(user?.totpEnabled)} username={user?.username} />
|
||||
</TabsContent>
|
||||
<TabsContent value="logs">
|
||||
<LogsContainer />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
33
src/app/(dashboard)/test-error/page.tsx
Normal file
33
src/app/(dashboard)/test-error/page.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import React from 'react';
|
||||
|
||||
export default function TestErrorPage() {
|
||||
const fetchFaultyApi = async () => {
|
||||
const response = await fetch('/api/test-error');
|
||||
const json = await response.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card d-flex">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
throw new Error('Sentry Frontend Error');
|
||||
}}
|
||||
>
|
||||
Throw error
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
await fetchFaultyApi();
|
||||
}}
|
||||
>
|
||||
Fetch faulty API
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
9
src/app/api/test-error/route.ts
Normal file
9
src/app/api/test-error/route.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { handleApiError } from '@/actions/utils/handle-api-error';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
throw new Error('API throw error test');
|
||||
} catch (error) {
|
||||
return handleApiError(error);
|
||||
}
|
||||
}
|
9
src/app/components/LogsTerminal/LogsTerminal.module.scss
Normal file
9
src/app/components/LogsTerminal/LogsTerminal.module.scss
Normal file
@ -0,0 +1,9 @@
|
||||
.logTerminal {
|
||||
height: 300px;
|
||||
overflow-y: scroll !important;
|
||||
scrollbar-color: rgb(94, 107, 133) rgb(29, 39, 59);
|
||||
}
|
||||
|
||||
.wrapLines {
|
||||
text-wrap: wrap;
|
||||
}
|
73
src/app/components/LogsTerminal/LogsTerminal.tsx
Normal file
73
src/app/components/LogsTerminal/LogsTerminal.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import styles from './LogsTerminal.module.scss';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
type Props = {
|
||||
logs: { id: number; text: string }[];
|
||||
maxLines: number;
|
||||
onMaxLinesChange: (lines: number) => void;
|
||||
};
|
||||
|
||||
export const LogsTerminal = (props: Props) => {
|
||||
const t = useTranslations();
|
||||
|
||||
const { logs, onMaxLinesChange, maxLines } = props;
|
||||
const [follow, setFollow] = useState<boolean>(true);
|
||||
const [wrapLines, setWrapLines] = useState<boolean>(false);
|
||||
const ref = useRef<HTMLPreElement>(null);
|
||||
|
||||
const lastLogId = logs.length > 0 ? logs.at(-1)?.id : null;
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && follow) {
|
||||
ref.current.scrollTop = ref.current.scrollHeight;
|
||||
}
|
||||
}, [lastLogId, follow]);
|
||||
|
||||
const updateMaxLines = (lines: number) => {
|
||||
const linesToKeep = Math.max(1, lines);
|
||||
onMaxLinesChange(linesToKeep);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="row d-flex align-items-center ps-1">
|
||||
<div className="col">
|
||||
<label className="form-check form-switch mt-1" htmlFor="follow-logs">
|
||||
<input id="follow-logs" className="form-check-input" type="checkbox" checked={follow} onChange={() => setFollow(!follow)} />
|
||||
<span className="form-check-label">{t('APP_LOGS_TAB_FOLLOW')}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="col">
|
||||
<label className="form-check form-switch mt-1" htmlFor="follow-logs">
|
||||
<input id="follow-logs" className="form-check-input" type="checkbox" checked={wrapLines} onChange={() => setWrapLines(!wrapLines)} />
|
||||
<span className="form-check-label">{t('APP_LOGS_TAB_WRAP_LINES')}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="input-group mb-2">
|
||||
<span className="input-group-text">{t('APP_LOGS_TAB_MAX_LINES')}</span>
|
||||
<input
|
||||
id="max-lines"
|
||||
type="number"
|
||||
className="form-control"
|
||||
value={maxLines}
|
||||
onChange={(e) => updateMaxLines(Number.parseInt(e.target.value, 10))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<pre id="log-terminal" className={clsx('mt-2', styles.logTerminal, { [styles.wrapLines || '']: wrapLines })} ref={ref}>
|
||||
{logs.map((log) => (
|
||||
<React.Fragment key={log.id}>
|
||||
{log.text}
|
||||
<br />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,19 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import Error from 'next/error';
|
||||
|
||||
export default function GlobalError({ error }) {
|
||||
useEffect(() => {
|
||||
Sentry.captureException(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<Error />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
20
src/app/global-error.tsx
Normal file
20
src/app/global-error.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import NextError from 'next/error';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function GlobalError({ error }: { error: Error & { digest?: string } }) {
|
||||
useEffect(() => {
|
||||
Sentry.captureException(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<NextError statusCode={undefined as never} />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
@ -79,6 +79,10 @@
|
||||
"APP_INSTALL_FORM_GENERAL": "General",
|
||||
"APP_INSTALL_FORM_REVERSE_PROXY": "Reverse proxy",
|
||||
"APP_INSTALL_SUCCESS": "App {id} installed successfully",
|
||||
"APP_LOGS_TAB_FOLLOW": "Follow logs",
|
||||
"APP_LOGS_TAB_MAX_LINES": "Max lines:",
|
||||
"APP_LOGS_TAB_TITLE": "Logs",
|
||||
"APP_LOGS_TAB_WRAP_LINES": "Wrap lines",
|
||||
"APP_NEW": "NEW",
|
||||
"APP_RESET_FORM_SUBMIT": "Reset",
|
||||
"APP_RESET_FORM_SUBTITLE": "All data for this app will be lost.",
|
||||
@ -294,6 +298,7 @@
|
||||
"SETTINGS_SECURITY_PASSWORD_NEEDED_HINT": "Your password is required to change two-factor authentication settings.",
|
||||
"SETTINGS_SECURITY_SCAN_QR_CODE": "Scan this QR code with your authenticator app.",
|
||||
"SETTINGS_SECURITY_TAB_TITLE": "Security",
|
||||
"SETTINGS_LOGS_TAB_TITLE": "Logs",
|
||||
"SETTINGS_TITLE": "Settings",
|
||||
"SYSTEM_ERROR_COULD_NOT_GET_LATEST_VERSION": "Could not get latest version",
|
||||
"SYSTEM_ERROR_CURRENT_VERSION_IS_LATEST": "Current version is already up to date",
|
||||
|
@ -77,6 +77,10 @@
|
||||
"APP_INSTALL_FORM_SUBMIT_UPDATE": "Actualizar",
|
||||
"APP_INSTALL_FORM_TITLE": "Instalar {name}",
|
||||
"APP_INSTALL_SUCCESS": "La aplicación {id} se instaló correctamente",
|
||||
"APP_LOGS_TAB_FOLLOW": "Seguir trazas",
|
||||
"APP_LOGS_TAB_MAX_LINES": "Maximo numero de líneas:",
|
||||
"APP_LOGS_TAB_TITLE": "Trazas",
|
||||
"APP_LOGS_TAB_WRAP_LINES": "Ajustar líneas",
|
||||
"APP_RESET_FORM_SUBMIT": "Restablecer",
|
||||
"APP_RESET_FORM_SUBTITLE": "Todos los datos de esta aplicación se perderán.",
|
||||
"APP_RESET_FORM_TITLE": "Restablecer {name}?",
|
||||
|
26
src/instrumentation.ts
Normal file
26
src/instrumentation.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import { cleanseErrorData } from '@runtipi/shared';
|
||||
|
||||
export async function register() {
|
||||
if (process.env.ALLOW_ERROR_MONITORING === 'true' && process.env.NODE_ENV === 'production' && process.env.LOCAL !== 'true') {
|
||||
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||
Sentry.init({
|
||||
environment: process.env.NODE_ENV,
|
||||
dsn: 'https://7a73d72f886948478b55621e7b92c3c7@o4504242900238336.ingest.sentry.io/4504826587971584',
|
||||
beforeSend: cleanseErrorData,
|
||||
integrations: [Sentry.extraErrorDataIntegration()],
|
||||
tracesSampleRate: 1.0,
|
||||
});
|
||||
}
|
||||
|
||||
if (process.env.NEXT_RUNTIME === 'edge') {
|
||||
Sentry.init({
|
||||
environment: process.env.NODE_ENV,
|
||||
dsn: 'https://7a73d72f886948478b55621e7b92c3c7@o4504242900238336.ingest.sentry.io/4504826587971584',
|
||||
beforeSend: cleanseErrorData,
|
||||
integrations: [Sentry.extraErrorDataIntegration()],
|
||||
tracesSampleRate: 1.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
113
src/lib/socket/useSocket.test.ts
Normal file
113
src/lib/socket/useSocket.test.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import { MockedFunction, afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { useSocket } from './useSocket'; // Adjust the path as needed
|
||||
import io, { Socket } from 'socket.io-client';
|
||||
import { renderHook, waitFor } from '@/tests/test-utils';
|
||||
|
||||
// Mocking socket.io-client
|
||||
vi.mock('socket.io-client');
|
||||
const mockedIo = io as unknown as MockedFunction<typeof io>;
|
||||
|
||||
// Utility functions to mock the socket.io behavior
|
||||
const mockSocket = {
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
emit: vi.fn(),
|
||||
connect: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
connected: true,
|
||||
disconnected: false,
|
||||
};
|
||||
|
||||
mockedIo.mockReturnValue(mockSocket as unknown as Socket);
|
||||
|
||||
describe('useSocket', () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should connect to the socket on mount', () => {
|
||||
const props = {
|
||||
selector: { type: 'runtipi-logs' },
|
||||
} as const;
|
||||
|
||||
renderHook(() => useSocket(props));
|
||||
|
||||
expect(mockedIo).toHaveBeenCalled();
|
||||
expect(mockSocket.on).toHaveBeenCalledWith('connect', expect.any(Function));
|
||||
});
|
||||
|
||||
it('should emit on connect if emitOnConnect is provided', () => {
|
||||
const emitOnConnect = { type: 'runtipi-logs', event: 'stopLogs', data: {} } as const;
|
||||
renderHook(() => useSocket({ selector: { type: 'runtipi-logs' }, emitOnConnect }));
|
||||
|
||||
expect(mockSocket.on).toHaveBeenCalledWith('connect', expect.any(Function));
|
||||
mockSocket.on.mock.calls.find((call) => call[0] === 'connect')[1](); // Call the connect handler
|
||||
|
||||
expect(mockSocket.emit).toHaveBeenCalledWith(emitOnConnect.type, emitOnConnect);
|
||||
});
|
||||
|
||||
it('should handle events according to the selector', () => {
|
||||
const eventData = { type: 'app-logs', event: 'newLogs', data: { appId: 'test' } };
|
||||
const onEvent = vi.fn();
|
||||
|
||||
renderHook(() => useSocket({ selector: { type: 'app-logs', event: 'newLogs' }, onEvent }));
|
||||
|
||||
expect(mockSocket.on).toHaveBeenCalledWith('app-logs', expect.any(Function));
|
||||
|
||||
mockSocket.on.mock.calls.find((call) => call[0] === 'app-logs')[1](eventData); // Call the event handler
|
||||
|
||||
expect(onEvent).toHaveBeenCalledWith('newLogs', eventData.data);
|
||||
});
|
||||
|
||||
it('should handle errors', () => {
|
||||
const onError = vi.fn();
|
||||
const errorMessage = 'test error';
|
||||
|
||||
renderHook(() =>
|
||||
useSocket({
|
||||
selector: { type: 'runtipi-logs' },
|
||||
onError,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockSocket.on).toHaveBeenCalledWith('error', expect.any(Function));
|
||||
mockSocket.on.mock.calls.find((call) => call[0] === 'error')[1](errorMessage); // Call the error handler
|
||||
|
||||
expect(onError).toHaveBeenCalledWith(errorMessage);
|
||||
});
|
||||
|
||||
it('should emit on disconnect if emitOnDisconnect is provided', () => {
|
||||
const { unmount } = renderHook(() =>
|
||||
useSocket({ selector: { type: 'app' }, emitOnDisconnect: { type: 'app', event: 'stop_error', data: { appId: 'test' } } }),
|
||||
);
|
||||
|
||||
unmount();
|
||||
|
||||
expect(mockSocket.emit).toHaveBeenCalledWith('app', { type: 'app', event: 'stop_error', data: { appId: 'test' } });
|
||||
expect(mockSocket.off).toHaveBeenCalledWith('app');
|
||||
expect(mockSocket.off).toHaveBeenCalledWith('error');
|
||||
});
|
||||
|
||||
it('should call onCleanup on unmount', () => {
|
||||
const onCleanup = vi.fn();
|
||||
|
||||
const { unmount } = renderHook(() => useSocket({ selector: { type: 'app-logs' }, onCleanup }));
|
||||
|
||||
unmount();
|
||||
|
||||
expect(onCleanup).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update lastData state on receiving data matching selector', async () => {
|
||||
const eventData = { type: 'app', event: 'stop_success', data: { appId: 'value' } };
|
||||
|
||||
const { result } = renderHook(() => useSocket({ selector: { type: 'app' } }));
|
||||
|
||||
expect(mockSocket.on).toHaveBeenCalledWith('app', expect.any(Function));
|
||||
mockSocket.on.mock.calls.find((call) => call[0] === 'app')[1](eventData); // Call the event handler
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.lastData).toEqual(eventData.data);
|
||||
});
|
||||
});
|
||||
});
|
@ -17,12 +17,15 @@ type Selector<T, U> = {
|
||||
type Props<T, U> = {
|
||||
onEvent?: (event: Extract<Extract<SocketEvent, { type: T }>['event'], U>, data: Extract<SocketEvent, { type: T }>['data']) => void;
|
||||
onError?: (error: string) => void;
|
||||
onCleanup?: () => void;
|
||||
emitOnConnect?: SocketEvent;
|
||||
emitOnDisconnect?: SocketEvent;
|
||||
initialData?: Extract<SocketEvent, { type: T }>['data'] | undefined;
|
||||
selector: Selector<T, U>;
|
||||
};
|
||||
|
||||
export const useSocket = <T extends SocketEvent['type'], U extends SocketEvent['event']>(props: Props<T, U>) => {
|
||||
const { onEvent, onError, selector, initialData } = props;
|
||||
const { onEvent, onError, onCleanup, selector, initialData, emitOnConnect, emitOnDisconnect } = props;
|
||||
const [lastData, setLastData] = useState(initialData as unknown);
|
||||
const socketRef = useRef<Socket>();
|
||||
|
||||
@ -37,6 +40,12 @@ export const useSocket = <T extends SocketEvent['type'], U extends SocketEvent['
|
||||
socketRef.current.connect();
|
||||
}
|
||||
|
||||
socketRef.current.on('connect', () => {
|
||||
if (emitOnConnect) {
|
||||
socketRef.current?.emit(emitOnConnect.type, emitOnConnect);
|
||||
}
|
||||
});
|
||||
|
||||
const handleEvent = (type: SocketEvent['type'], rawData: unknown) => {
|
||||
const parsedEvent = socketEventSchema.safeParse(rawData);
|
||||
|
||||
@ -56,7 +65,7 @@ export const useSocket = <T extends SocketEvent['type'], U extends SocketEvent['
|
||||
}
|
||||
|
||||
const property = selector.data?.property as keyof SocketEvent['data'];
|
||||
if (selector.data && selector.data.value !== data[property]) {
|
||||
if (selector.data && data && selector.data.value !== data[property]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -76,11 +85,17 @@ export const useSocket = <T extends SocketEvent['type'], U extends SocketEvent['
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (emitOnDisconnect) {
|
||||
socketRef.current?.emit(emitOnDisconnect.type, emitOnDisconnect);
|
||||
}
|
||||
|
||||
socketRef.current?.off(selector.type as string);
|
||||
socketRef.current?.off('error');
|
||||
socketRef.current = undefined;
|
||||
onCleanup?.();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- This effect should never re-run
|
||||
}, []);
|
||||
|
||||
return { lastData } as { lastData: Extract<SocketEvent, { type: T }>['data'] | undefined };
|
||||
return { lastData, socket: socketRef.current } as { lastData: Extract<SocketEvent, { type: T }>['data'] | undefined; socket: Socket | undefined };
|
||||
};
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { defineWorkspace } from 'vitest/config';
|
||||
import { UserWorkspaceConfig, defineWorkspace } from 'vitest/config';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
type Plugins = UserWorkspaceConfig['plugins'];
|
||||
|
||||
export default defineWorkspace([
|
||||
{
|
||||
plugins: [tsconfigPaths()],
|
||||
plugins: [tsconfigPaths()] as Plugins,
|
||||
test: {
|
||||
globals: true,
|
||||
name: 'server',
|
||||
@ -16,13 +18,13 @@ export default defineWorkspace([
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: [tsconfigPaths(), react()],
|
||||
plugins: [tsconfigPaths(), react()] as Plugins,
|
||||
test: {
|
||||
globals: true,
|
||||
name: 'client',
|
||||
root: './',
|
||||
environment: 'jsdom',
|
||||
include: ['./src/client/**/*.{spec,test}.{ts,tsx}', './src/app/**/*.{spec,test}.{ts,tsx}'],
|
||||
include: ['./src/client/**/*.{spec,test}.{ts,tsx}', './src/app/**/*.{spec,test}.{ts,tsx}', './src/lib/**/*.{spec,test}.{ts,tsx}'],
|
||||
setupFiles: ['./tests/client/test.setup.tsx'],
|
||||
},
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user