Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9aa92da633 |
@@ -139,7 +139,8 @@ jobs:
|
||||
branch_build_push_admin:
|
||||
name: Build-Push Admin Docker Image
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [branch_build_setup]
|
||||
needs:
|
||||
- branch_build_setup
|
||||
steps:
|
||||
- name: Admin Build and Push
|
||||
uses: makeplane/actions/build-push@v1.0.0
|
||||
@@ -152,7 +153,9 @@ jobs:
|
||||
docker-image-owner: makeplane
|
||||
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_admin }}
|
||||
build-context: .
|
||||
dockerfile-path: ./apps/admin/Dockerfile.admin
|
||||
dockerfile-path: ./Dockerfile.node
|
||||
build-args: |
|
||||
APP_SCOPE=admin
|
||||
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
|
||||
@@ -161,7 +164,8 @@ jobs:
|
||||
branch_build_push_web:
|
||||
name: Build-Push Web Docker Image
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [branch_build_setup]
|
||||
needs:
|
||||
- branch_build_setup
|
||||
steps:
|
||||
- name: Web Build and Push
|
||||
uses: makeplane/actions/build-push@v1.0.0
|
||||
@@ -174,7 +178,9 @@ jobs:
|
||||
docker-image-owner: makeplane
|
||||
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_web }}
|
||||
build-context: .
|
||||
dockerfile-path: ./apps/web/Dockerfile.web
|
||||
dockerfile-path: ./Dockerfile.node
|
||||
build-args: |
|
||||
APP_SCOPE=web
|
||||
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
|
||||
@@ -183,7 +189,8 @@ jobs:
|
||||
branch_build_push_space:
|
||||
name: Build-Push Space Docker Image
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [branch_build_setup]
|
||||
needs:
|
||||
- branch_build_setup
|
||||
steps:
|
||||
- name: Space Build and Push
|
||||
uses: makeplane/actions/build-push@v1.0.0
|
||||
@@ -196,7 +203,9 @@ jobs:
|
||||
docker-image-owner: makeplane
|
||||
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_space }}
|
||||
build-context: .
|
||||
dockerfile-path: ./apps/space/Dockerfile.space
|
||||
dockerfile-path: ./Dockerfile.node
|
||||
build-args: |
|
||||
APP_SCOPE=space
|
||||
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
|
||||
@@ -205,7 +214,8 @@ jobs:
|
||||
branch_build_push_live:
|
||||
name: Build-Push Live Collaboration Docker Image
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [branch_build_setup]
|
||||
needs:
|
||||
- branch_build_setup
|
||||
steps:
|
||||
- name: Live Build and Push
|
||||
uses: makeplane/actions/build-push@v1.0.0
|
||||
@@ -227,7 +237,8 @@ jobs:
|
||||
branch_build_push_api:
|
||||
name: Build-Push API Server Docker Image
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [branch_build_setup]
|
||||
needs:
|
||||
- branch_build_setup
|
||||
steps:
|
||||
- name: Backend Build and Push
|
||||
uses: makeplane/actions/build-push@v1.0.0
|
||||
@@ -239,8 +250,8 @@ jobs:
|
||||
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
docker-image-owner: makeplane
|
||||
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_backend }}
|
||||
build-context: ./apps/api
|
||||
dockerfile-path: ./apps/api/Dockerfile.api
|
||||
build-context: .
|
||||
dockerfile-path: ./Dockerfile.api
|
||||
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
|
||||
@@ -249,7 +260,8 @@ jobs:
|
||||
branch_build_push_proxy:
|
||||
name: Build-Push Proxy Docker Image
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [branch_build_setup]
|
||||
needs:
|
||||
- branch_build_setup
|
||||
steps:
|
||||
- name: Proxy Build and Push
|
||||
uses: makeplane/actions/build-push@v1.0.0
|
||||
@@ -368,15 +380,13 @@ jobs:
|
||||
name: Build Release
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
[
|
||||
branch_build_setup,
|
||||
branch_build_push_admin,
|
||||
branch_build_push_web,
|
||||
branch_build_push_space,
|
||||
branch_build_push_live,
|
||||
branch_build_push_api,
|
||||
branch_build_push_proxy,
|
||||
]
|
||||
- branch_build_setup
|
||||
- branch_build_push_admin
|
||||
- branch_build_push_web
|
||||
- branch_build_push_space
|
||||
- branch_build_push_live
|
||||
- branch_build_push_api
|
||||
- branch_build_push_proxy
|
||||
env:
|
||||
REL_VERSION: ${{ needs.branch_build_setup.outputs.release_version }}
|
||||
steps:
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
name: Docker AIO build and smoke test
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- "preview"
|
||||
paths:
|
||||
- "apps/web/**"
|
||||
- "apps/space/**"
|
||||
- "apps/admin/**"
|
||||
- "apps/live/**"
|
||||
- "packages/**"
|
||||
- "turbo.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "Dockerfile.node"
|
||||
- "Dockerfile.api"
|
||||
- "Dockerfile.aio"
|
||||
- "docker-bake.hcl"
|
||||
- ".github/workflows/docker-smoke-aio.yml"
|
||||
push:
|
||||
branches:
|
||||
- "preview"
|
||||
paths:
|
||||
- "apps/web/**"
|
||||
- "apps/space/**"
|
||||
- "apps/admin/**"
|
||||
- "apps/live/**"
|
||||
- "packages/**"
|
||||
- "turbo.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "Dockerfile.node"
|
||||
- "Dockerfile.api"
|
||||
- "Dockerfile.aio"
|
||||
- "docker-bake.hcl"
|
||||
- ".github/workflows/docker-smoke-aio.yml"
|
||||
|
||||
concurrency:
|
||||
group: aio-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
determine-aio:
|
||||
name: Determine if AIO needed
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
aio_needed: ${{ steps.build-flag.outputs.aio_needed }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Detect changed paths
|
||||
id: changes
|
||||
uses: dorny/paths-filter@v3
|
||||
with:
|
||||
filters: |
|
||||
web:
|
||||
- 'apps/web/**'
|
||||
space:
|
||||
- 'apps/space/**'
|
||||
admin:
|
||||
- 'apps/admin/**'
|
||||
live:
|
||||
- 'apps/live/**'
|
||||
common:
|
||||
- 'packages/**'
|
||||
- 'turbo.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
- 'Dockerfile.node'
|
||||
- 'Dockerfile.api'
|
||||
- 'Dockerfile.aio'
|
||||
- 'docker-bake.hcl'
|
||||
- '.github/workflows/docker-smoke-aio.yml'
|
||||
|
||||
- name: Compute AIO flag
|
||||
id: build-flag
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const anyCommon = '${{ steps.changes.outputs.common }}' === 'true';
|
||||
const changedWeb = '${{ steps.changes.outputs.web }}' === 'true';
|
||||
const changedSpace = '${{ steps.changes.outputs.space }}' === 'true';
|
||||
const changedAdmin = '${{ steps.changes.outputs.admin }}' === 'true';
|
||||
const changedLive = '${{ steps.changes.outputs.live }}' === 'true';
|
||||
const aioNeeded = anyCommon || changedWeb || changedSpace || changedAdmin || changedLive;
|
||||
core.setOutput('aio_needed', String(aioNeeded));
|
||||
|
||||
aio_smoke:
|
||||
name: Build and smoke test AIO
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine-aio
|
||||
if: ${{ needs.determine-aio.outputs.aio_needed == 'true' }}
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Show Docker version
|
||||
run: |
|
||||
docker version
|
||||
docker info
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build AIO image with bake (load into local daemon)
|
||||
run: |
|
||||
docker buildx bake -f "./docker-bake.hcl" --load aio
|
||||
|
||||
- name: Run AIO smoke script
|
||||
run: |
|
||||
chmod +x "scripts/smoke-aio.sh"
|
||||
"scripts/smoke-aio.sh"
|
||||
@@ -0,0 +1,171 @@
|
||||
name: Docker build and smoke test for apps
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- "preview"
|
||||
paths:
|
||||
- "apps/web/**"
|
||||
- "apps/space/**"
|
||||
- "apps/admin/**"
|
||||
- "apps/live/**"
|
||||
- "packages/**"
|
||||
- "turbo.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "Dockerfile.node"
|
||||
- "Dockerfile.api"
|
||||
- "Dockerfile.aio"
|
||||
- "docker-bake.hcl"
|
||||
|
||||
- ".github/workflows/docker-smoke.yml"
|
||||
push:
|
||||
branches:
|
||||
- "preview"
|
||||
paths:
|
||||
- "apps/web/**"
|
||||
- "apps/space/**"
|
||||
- "apps/admin/**"
|
||||
- "apps/live/**"
|
||||
- "packages/**"
|
||||
- "turbo.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "Dockerfile.node"
|
||||
- "Dockerfile.api"
|
||||
- "Dockerfile.aio"
|
||||
- "docker-bake.hcl"
|
||||
|
||||
- ".github/workflows/docker-smoke.yml"
|
||||
|
||||
jobs:
|
||||
determine-matrix:
|
||||
name: Determine matrix
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.build-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Detect changed paths
|
||||
id: changes
|
||||
uses: dorny/paths-filter@v3
|
||||
with:
|
||||
filters: |
|
||||
web:
|
||||
- 'apps/web/**'
|
||||
space:
|
||||
- 'apps/space/**'
|
||||
admin:
|
||||
- 'apps/admin/**'
|
||||
live:
|
||||
- 'apps/live/**'
|
||||
common:
|
||||
- 'packages/**'
|
||||
- 'turbo.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
- 'Dockerfile.node'
|
||||
- 'Dockerfile.api'
|
||||
- 'Dockerfile.aio'
|
||||
- 'docker-bake.hcl'
|
||||
- '.github/workflows/docker-smoke.yml'
|
||||
|
||||
- name: Build matrix
|
||||
id: build-matrix
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const include = [];
|
||||
const anyCommon = '${{ steps.changes.outputs.common }}' === 'true';
|
||||
const changed = {
|
||||
web: '${{ steps.changes.outputs.web }}' === 'true',
|
||||
space: '${{ steps.changes.outputs.space }}' === 'true',
|
||||
admin: '${{ steps.changes.outputs.admin }}' === 'true',
|
||||
live: '${{ steps.changes.outputs.live }}' === 'true',
|
||||
};
|
||||
const add = (name, bake_target, image, container, port, path, env_flags = "") =>
|
||||
include.push({ name, bake_target, image, container, host_port: port, path, env_flags });
|
||||
|
||||
const buildAll = anyCommon || changed.web || changed.space || changed.admin || changed.live;
|
||||
if (buildAll || changed.web) add('web', 'web', 'plane-web:ci-smoke', 'plane-web-ci', 3001, '/');
|
||||
if (buildAll || changed.space) add('space', 'space', 'plane-space:ci-smoke', 'plane-space-ci', 3002, '/spaces');
|
||||
if (buildAll || changed.admin) add('admin', 'admin', 'plane-admin:ci-smoke', 'plane-admin-ci', 3003, '/god-mode');
|
||||
if (buildAll || changed.live) add('live', 'live', 'plane-live:ci-smoke', 'plane-live-ci', 3005, '/live/health', '-e NODE_ENV=production -e LIVE_BASE_PATH=/live');
|
||||
|
||||
if (include.length === 0) {
|
||||
// Default to web to keep job non-empty
|
||||
add('web', 'web', 'plane-web:ci-smoke', 'plane-web-ci', 3001, '/');
|
||||
}
|
||||
core.setOutput('matrix', JSON.stringify({ include }));
|
||||
|
||||
smoke:
|
||||
name: Build and smoke test ${{ matrix.name }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine-matrix
|
||||
timeout-minutes: 25
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.determine-matrix.outputs.matrix) }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Prepare build environment
|
||||
run: echo "Using docker build (no buildx bake)"
|
||||
|
||||
- name: Show Docker version
|
||||
run: |
|
||||
docker version
|
||||
docker info
|
||||
|
||||
- name: Build image (${{ matrix.name }})
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
name="${{ matrix.name }}"
|
||||
tag="${{ matrix.image }}"
|
||||
if [ "$name" = "live" ]; then
|
||||
docker build -f "apps/live/Dockerfile.live" -t "$tag" "."
|
||||
else
|
||||
docker build -f "Dockerfile.node" --target runtime --build-arg APP_SCOPE="$name" -t "$tag" "."
|
||||
fi
|
||||
|
||||
- name: Run container (${{ matrix.name }})
|
||||
run: |
|
||||
docker run -d --name ${{ matrix.container }} -p ${{ matrix.host_port }}:3000 ${{ matrix.env_flags }} ${{ matrix.image }}
|
||||
docker ps -a
|
||||
|
||||
- name: Smoke test HTTP endpoint (${{ matrix.name }})
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
URL="http://localhost:${{ matrix.host_port }}${{ matrix.path }}"
|
||||
echo "Probing $URL ..."
|
||||
for i in {1..60}; do
|
||||
STATUS="$(curl -sS -o /dev/null -w "%{http_code}" -L "${URL}" || true)"
|
||||
if [ "${STATUS}" = "200" ]; then
|
||||
echo "Success: HTTP ${STATUS} from ${URL}"
|
||||
exit 0
|
||||
fi
|
||||
echo "Attempt ${i}: HTTP ${STATUS} (waiting 2s)"
|
||||
sleep 2
|
||||
done
|
||||
echo "Failed to get HTTP 200 from ${URL}"
|
||||
echo "::group::Container logs (${{ matrix.container }})"
|
||||
docker logs ${{ matrix.container }} || true
|
||||
echo "::endgroup::"
|
||||
exit 1
|
||||
|
||||
- name: Cleanup container (${{ matrix.name }})
|
||||
if: always()
|
||||
run: |
|
||||
docker rm -f ${{ matrix.container }} || true
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
#
|
||||
# All-in-one assembler image that composes Plane runtime from per-app images
|
||||
# and the community supervisor/Caddy configuration.
|
||||
#
|
||||
# Build requirements:
|
||||
# - Build and tag the per-app images first (or override args below):
|
||||
# plane-web:latest, plane-space:latest, plane-admin:latest, plane-live:latest,
|
||||
# plane-api:latest, plane-proxy:latest
|
||||
#
|
||||
# Example:
|
||||
# docker build \
|
||||
# -f plane/Dockerfile.aio \
|
||||
# --build-arg WEB_IMG=plane-web:latest \
|
||||
# --build-arg SPACE_IMG=plane-space:latest \
|
||||
# --build-arg ADMIN_IMG=plane-admin:latest \
|
||||
# --build-arg LIVE_IMG=plane-live:latest \
|
||||
# --build-arg API_IMG=plane-api:latest \
|
||||
# --build-arg PROXY_IMG=plane-proxy:latest \
|
||||
# -t plane-aio:latest .
|
||||
#
|
||||
# Run:
|
||||
# docker run --rm -it -p 80:80 plane-aio:latest
|
||||
#
|
||||
# Provide required env vars; see deployments/aio/community/README.md.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Arguments to reference locally-built component images
|
||||
# ------------------------------------------------------------------------------
|
||||
# Build contexts are used for component images:
|
||||
# --build-context web_ctx=target:web
|
||||
# --build-context space_ctx=target:space
|
||||
# --build-context admin_ctx=target:admin
|
||||
# --build-context live_ctx=target:live
|
||||
# --build-context api_ctx=target:api
|
||||
# --build-context proxy_ctx=target:proxy
|
||||
ARG NODE_VERSION=22-alpine
|
||||
ARG PY_VERSION=3.12.10-alpine
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Source stages: pull artifacts from pre-built images
|
||||
# ------------------------------------------------------------------------------
|
||||
FROM node:${NODE_VERSION} AS node
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Final runner: Python as base (Supervisor + API deps live here)
|
||||
# ------------------------------------------------------------------------------
|
||||
FROM python:${PY_VERSION} AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Base system libs for API/Caddy/Node runtime
|
||||
RUN apk add --no-cache \
|
||||
libpq \
|
||||
libxslt \
|
||||
xmlsec \
|
||||
nss-tools \
|
||||
bash \
|
||||
curl \
|
||||
ca-certificates \
|
||||
openssl
|
||||
|
||||
# Install supervisor
|
||||
RUN pip install --no-cache-dir supervisor && mkdir -p /etc/supervisor/conf.d
|
||||
|
||||
# Copy Node runtime into the AIO container for Next standalone apps
|
||||
COPY --from=node /usr/lib /usr/lib
|
||||
COPY --from=node /usr/local/lib /usr/local/lib
|
||||
COPY --from=node /usr/local/include /usr/local/include
|
||||
COPY --from=node /usr/local/bin /usr/local/bin
|
||||
|
||||
# Copy Next.js standalone app artifacts (namespaced under /app/<app>)
|
||||
COPY --from=web_ctx /app /app/web
|
||||
COPY --from=space_ctx /app /app/space
|
||||
COPY --from=admin_ctx /app /app/admin
|
||||
COPY --from=live_ctx /app /app/live
|
||||
|
||||
# Clean potential Next caches (optional)
|
||||
RUN rm -rf /app/web/apps/web/.next/cache || true \
|
||||
&& rm -rf /app/space/apps/space/.next/cache || true \
|
||||
&& rm -rf /app/admin/apps/admin/.next/cache || true
|
||||
|
||||
# Copy Python backend code and installed packages/binaries
|
||||
COPY --from=api_ctx /code /app/backend
|
||||
# Match CPython version path (PY_VERSION=3.12.x-alpine)
|
||||
COPY --from=api_ctx /usr/local/lib/python3.12/site-packages/ /usr/local/lib/python3.12/site-packages/
|
||||
COPY --from=api_ctx /usr/local/bin/ /usr/local/bin/
|
||||
|
||||
# Caddy binary from proxy image
|
||||
COPY --from=proxy_ctx /usr/bin/caddy /usr/bin/caddy
|
||||
|
||||
# Community supervisor config and startup script
|
||||
COPY deployments/aio/community/start.sh /app/start.sh
|
||||
COPY deployments/aio/community/supervisor.conf /etc/supervisor/conf.d/supervisor.conf
|
||||
|
||||
# Provide a default plane.env from variables template; start.sh will update it
|
||||
COPY deployments/aio/community/variables.env /app/plane.env
|
||||
|
||||
# Prepare Caddy configuration:
|
||||
# - Start from the repository Caddyfile.ce
|
||||
# - Update upstreams to localhost-bound ports managed by supervisor
|
||||
RUN mkdir -p /app/proxy
|
||||
COPY apps/proxy/Caddyfile.ce /app/proxy/Caddyfile
|
||||
RUN set -eux; \
|
||||
sed -i 's|web:3000|localhost:3001|g' /app/proxy/Caddyfile; \
|
||||
sed -i 's|space:3000|localhost:3002|g' /app/proxy/Caddyfile; \
|
||||
sed -i 's|admin:3000|localhost:3003|g' /app/proxy/Caddyfile; \
|
||||
sed -i 's|api:8000|localhost:3004|g' /app/proxy/Caddyfile; \
|
||||
sed -i 's|live:3000|localhost:3005|g' /app/proxy/Caddyfile; \
|
||||
sed -i '/plane-minio:9000/d' /app/proxy/Caddyfile
|
||||
|
||||
# Folders and permissions
|
||||
RUN mkdir -p /app/logs/access /app/logs/error /app/data \
|
||||
&& chmod +x /app/start.sh
|
||||
|
||||
VOLUME ["/app/data", "/app/logs"]
|
||||
|
||||
EXPOSE 80 443
|
||||
|
||||
CMD ["/app/start.sh"]
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
#
|
||||
# Unified multi-stage Dockerfile for the Python API
|
||||
# Targets:
|
||||
# - dev: local development with hot reload and full build toolchain
|
||||
# - runtime: production runtime using prebuilt wheels
|
||||
#
|
||||
# Example builds (from repo root):
|
||||
# docker build -f plane/Dockerfile.api --target dev -t plane-api:dev .
|
||||
# docker build -f plane/Dockerfile.api --target runtime -t plane-api:latest .
|
||||
|
||||
ARG PY_VERSION=python:3.12.10-alpine
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# base: common runtime base (no build toolchain)
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM ${PY_VERSION} AS base
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
||||
INSTANCE_CHANGELOG_URL=https://sites.plane.so/pages/691ef037bcfe416a902e48cb55f59891/
|
||||
|
||||
# Runtime libraries required by the app
|
||||
RUN apk add --no-cache \
|
||||
libpq \
|
||||
libxslt \
|
||||
xmlsec \
|
||||
ca-certificates \
|
||||
openssl
|
||||
|
||||
WORKDIR /code
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# builder: build Python wheels for all dependencies
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM ${PY_VERSION} AS builder
|
||||
|
||||
# Full build toolchain to compile deps to wheels
|
||||
RUN apk add --no-cache \
|
||||
bash~=5.2 \
|
||||
g++ \
|
||||
gcc \
|
||||
cargo \
|
||||
git \
|
||||
make \
|
||||
postgresql-dev \
|
||||
libc-dev \
|
||||
linux-headers \
|
||||
libffi-dev \
|
||||
libxml2-dev \
|
||||
libxslt-dev \
|
||||
openssl-dev \
|
||||
xmlsec-dev
|
||||
|
||||
WORKDIR /w
|
||||
|
||||
# Copy requirements (relative to repo root)
|
||||
COPY apps/api/requirements.txt /w/requirements.txt
|
||||
COPY apps/api/requirements /w/requirements
|
||||
|
||||
# Build wheels into /wheels to reuse in runtime
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
pip wheel -r /w/requirements.txt --wheel-dir /wheels
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# dev: local development image (bind mount source into /code)
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM ${PY_VERSION} AS dev
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
||||
INSTANCE_CHANGELOG_URL=https://sites.plane.so/pages/691ef037bcfe416a902e48cb55f59891/
|
||||
|
||||
# Match prior dev environment: runtime + build deps + node
|
||||
RUN apk add --no-cache \
|
||||
libpq \
|
||||
libxslt \
|
||||
xmlsec \
|
||||
nodejs-current \
|
||||
bash~=5.2 \
|
||||
g++ \
|
||||
gcc \
|
||||
cargo \
|
||||
git \
|
||||
make \
|
||||
postgresql-dev \
|
||||
libc-dev \
|
||||
linux-headers \
|
||||
libffi-dev
|
||||
|
||||
WORKDIR /code
|
||||
|
||||
# Copy and install local dev requirements
|
||||
COPY apps/api/requirements.txt ./requirements.txt
|
||||
COPY apps/api/requirements ./requirements
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
pip install -r requirements/local.txt --no-cache-dir
|
||||
|
||||
# Bring in the API source
|
||||
COPY apps/api/ ./
|
||||
|
||||
# Permissions similar to existing Dockerfiles
|
||||
RUN mkdir -p /code/plane/logs \
|
||||
&& chmod -R +x /code/bin \
|
||||
&& chmod -R 777 /code
|
||||
|
||||
EXPOSE 8000
|
||||
CMD ["./bin/docker-entrypoint-api-local.sh"]
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# runtime: production image using wheels from builder
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM base AS runtime
|
||||
|
||||
# Bash needed for entrypoint scripts
|
||||
RUN apk add --no-cache bash~=5.2
|
||||
|
||||
WORKDIR /code
|
||||
|
||||
# Install from wheels for reproducible, fast builds
|
||||
COPY --from=builder /wheels /wheels
|
||||
COPY apps/api/requirements.txt ./requirements.txt
|
||||
COPY apps/api/requirements ./requirements
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
pip install --no-cache-dir --no-index --find-links=/wheels -r requirements.txt
|
||||
|
||||
# Copy only what is required to run
|
||||
COPY apps/api/manage.py ./manage.py
|
||||
COPY apps/api/plane ./plane
|
||||
COPY apps/api/templates ./templates
|
||||
COPY apps/api/package.json ./package.json
|
||||
COPY apps/api/bin ./bin
|
||||
|
||||
# Create unprivileged user and set secure permissions
|
||||
RUN addgroup -S plane && adduser -S -G plane -h /code -s /sbin/nologin plane \
|
||||
&& chmod +x ./bin/* \
|
||||
&& mkdir -p /code/plane/logs \
|
||||
&& chown -R plane:plane /code \
|
||||
&& chmod -R 755 /code \
|
||||
&& chmod 775 /code/plane/logs
|
||||
|
||||
ENV GUNICORN_WORKERS=3 \
|
||||
PORT=8000
|
||||
|
||||
USER plane
|
||||
EXPOSE 8000
|
||||
|
||||
# Default: API server; override command for worker/beat/migrator as needed
|
||||
CMD ["./bin/docker-entrypoint-api.sh"]
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
#
|
||||
# Unified multi-stage Dockerfile for Next.js apps in this monorepo.
|
||||
# Supports:
|
||||
# - target=dev (hot-reload, expects bind-mounted repo)
|
||||
# - target=runtime (production image using Next.js standalone output)
|
||||
#
|
||||
# Usage examples:
|
||||
# - Build dev image for web: docker build --target dev --build-arg APP_SCOPE=web -t plane-web:dev .
|
||||
# - Run dev (with compose): mount the repo into /repo and override command/ports as needed
|
||||
# - Build prod image for web: docker build --target runtime --build-arg APP_SCOPE=web -t plane-web:latest .
|
||||
#
|
||||
# APP_SCOPE must be one of: web, space, admin
|
||||
|
||||
ARG NODE_VERSION=22-alpine
|
||||
ARG TURBO_VERSION=2.5.6
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Base: Node + pnpm (corepack)
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM node:${NODE_VERSION} AS base
|
||||
|
||||
ENV PNPM_HOME=/pnpm
|
||||
ENV PATH=$PNPM_HOME:$PATH
|
||||
RUN corepack enable && apk add --no-cache libc6-compat
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# builder: prune workspace using turbo for the selected app scope
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM base AS builder
|
||||
ARG APP_SCOPE
|
||||
WORKDIR /repo
|
||||
|
||||
# Full context is required for turbo prune
|
||||
COPY . .
|
||||
RUN pnpm add -g turbo@${TURBO_VERSION}
|
||||
RUN turbo prune --scope=${APP_SCOPE} --docker
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# installer: install deps (offline) and build the selected scope
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM base AS installer
|
||||
WORKDIR /repo
|
||||
|
||||
# Seed minimal files for reproducible installs and good caching
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=builder /repo/out/json/ .
|
||||
COPY --from=builder /repo/out/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||
|
||||
# Fetch dependencies into a cached store layer
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \
|
||||
pnpm fetch --store-dir=/pnpm/store
|
||||
|
||||
# Bring in only the pruned workspace for fast installs/builds
|
||||
COPY --from=builder /repo/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
|
||||
# Offline, frozen lockfile install from cached store
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \
|
||||
pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store
|
||||
|
||||
# Build-time environment (safe to pass through; only NEXT_PUBLIC_* are embedded)
|
||||
# Keep parity with existing app Dockerfiles
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_WEB_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV TURBO_TELEMETRY_DISABLED=1
|
||||
|
||||
# Build only the selected scope
|
||||
ARG APP_SCOPE
|
||||
RUN pnpm turbo run build --filter=${APP_SCOPE}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# dev: for local development with hot reload (bind-mount the repo to /repo)
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM base AS dev
|
||||
WORKDIR /repo
|
||||
|
||||
# Helpful global tool for selective builds/dev
|
||||
RUN pnpm add -g turbo@${TURBO_VERSION}
|
||||
|
||||
ENV NODE_ENV=development \
|
||||
NEXT_TELEMETRY_DISABLED=1 \
|
||||
TURBO_TELEMETRY_DISABLED=1
|
||||
|
||||
# Select which app to run in dev (web|space|admin)
|
||||
ARG APP_SCOPE
|
||||
ENV APP_SCOPE=${APP_SCOPE}
|
||||
|
||||
EXPOSE 3000
|
||||
# Expect the source to be bind-mounted; install deps and start dev server
|
||||
CMD ["sh", "-lc", "pnpm install && pnpm dev --filter=${APP_SCOPE}"]
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# runtime: minimal Next.js standalone runner for the selected scope
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM node:${NODE_VERSION} AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production \
|
||||
NEXT_TELEMETRY_DISABLED=1 \
|
||||
TURBO_TELEMETRY_DISABLED=1 \
|
||||
HOSTNAME=0.0.0.0 \
|
||||
PORT=3000
|
||||
|
||||
# Drop privileges
|
||||
RUN addgroup -S -g 1001 nodejs \
|
||||
&& adduser -S -u 1001 -G nodejs -h /home/nextjs -D nextjs \
|
||||
&& mkdir -p /home/nextjs \
|
||||
&& chown -R nextjs:nodejs /home/nextjs
|
||||
USER nextjs
|
||||
|
||||
# Copy only the built output for the selected scope
|
||||
ARG APP_SCOPE
|
||||
ENV APP_SCOPE=${APP_SCOPE}
|
||||
# Next.js standalone layout
|
||||
COPY --from=installer /repo/apps/${APP_SCOPE}/.next/standalone ./
|
||||
COPY --from=installer /repo/apps/${APP_SCOPE}/.next/static ./apps/${APP_SCOPE}/.next/static
|
||||
COPY --from=installer /repo/apps/${APP_SCOPE}/public ./apps/${APP_SCOPE}/public
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["sh", "-lc", "set -e; : \"${APP_SCOPE:?APP_SCOPE env is required}\"; SERVER=\"apps/${APP_SCOPE}/server.js\"; if [ ! -f \"$SERVER\" ]; then echo \"Error: $SERVER not found.\"; ls -la \"apps/${APP_SCOPE}\" || true; echo \"Known apps:\"; ls -1 apps || true; exit 1; fi; exec node \"$SERVER\""]
|
||||
@@ -1,12 +0,0 @@
|
||||
.next/*
|
||||
out/*
|
||||
public/*
|
||||
dist/*
|
||||
node_modules/*
|
||||
.turbo/*
|
||||
.env*
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.production
|
||||
.env.test
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/next.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
FROM node:22-alpine AS base
|
||||
|
||||
# Setup pnpm package manager with corepack and configure global bin directory for caching
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 1: Build the project
|
||||
# *****************************************************************************
|
||||
FROM base AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
ARG TURBO_VERSION=2.5.6
|
||||
RUN corepack enable pnpm && pnpm add -g turbo@${TURBO_VERSION}
|
||||
COPY . .
|
||||
|
||||
RUN turbo prune --scope=admin --docker
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 2: Install dependencies & build the project
|
||||
# *****************************************************************************
|
||||
FROM base AS installer
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=builder /app/out/json/ .
|
||||
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||
RUN corepack enable pnpm
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/pnpm/store
|
||||
|
||||
COPY --from=builder /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_WEB_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV TURBO_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN pnpm turbo run build --filter=admin
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 3: Copy the project and start it
|
||||
# *****************************************************************************
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
# Don't run production as root
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
USER nextjs
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=installer /app/apps/admin/.next/standalone ./
|
||||
COPY --from=installer /app/apps/admin/.next/static ./apps/admin/.next/static
|
||||
COPY --from=installer /app/apps/admin/public ./apps/admin/public
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_WEB_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV TURBO_TELEMETRY_DISABLED=1
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "apps/admin/server.js"]
|
||||
@@ -1,17 +0,0 @@
|
||||
FROM node:22-alpine
|
||||
RUN apk add --no-cache libc6-compat
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN corepack enable pnpm && pnpm add -g turbo
|
||||
RUN pnpm install
|
||||
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
VOLUME [ "/app/node_modules", "/app/admin/node_modules" ]
|
||||
|
||||
CMD ["pnpm", "dev", "--filter=admin"]
|
||||
@@ -9,7 +9,7 @@ import { useInstance } from "@/hooks/store";
|
||||
// components
|
||||
import { InstanceEmailForm } from "./email-config-form";
|
||||
|
||||
const InstanceEmailPage: React.FC = observer(() => {
|
||||
const InstanceEmailPage = observer(() => {
|
||||
// store
|
||||
const { fetchInstanceConfigurations, formattedConfig, disableEmail } = useInstance();
|
||||
|
||||
@@ -29,7 +29,7 @@ const InstanceEmailPage: React.FC = observer(() => {
|
||||
message: "Email feature has been disabled",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
});
|
||||
} catch (_error) {
|
||||
} catch (error) {
|
||||
setToast({
|
||||
title: "Error disabling email",
|
||||
message: "Failed to disable email feature. Please try again.",
|
||||
|
||||
@@ -25,8 +25,9 @@ export const EmailCodesConfiguration: React.FC<Props> = observer((props) => {
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableMagicLogin))}
|
||||
onChange={() => {
|
||||
const newEnableMagicLogin = Boolean(parseInt(enableMagicLogin)) === true ? "0" : "1";
|
||||
updateConfig("ENABLE_MAGIC_LINK_LOGIN", newEnableMagicLogin);
|
||||
Boolean(parseInt(enableMagicLogin)) === true
|
||||
? updateConfig("ENABLE_MAGIC_LINK_LOGIN", "0")
|
||||
: updateConfig("ENABLE_MAGIC_LINK_LOGIN", "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
|
||||
@@ -35,8 +35,9 @@ export const GithubConfiguration: React.FC<Props> = observer((props) => {
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableGithubConfig))}
|
||||
onChange={() => {
|
||||
const newEnableGithubConfig = Boolean(parseInt(enableGithubConfig)) === true ? "0" : "1";
|
||||
updateConfig("IS_GITHUB_ENABLED", newEnableGithubConfig);
|
||||
Boolean(parseInt(enableGithubConfig)) === true
|
||||
? updateConfig("IS_GITHUB_ENABLED", "0")
|
||||
: updateConfig("IS_GITHUB_ENABLED", "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
|
||||
@@ -35,8 +35,9 @@ export const GitlabConfiguration: React.FC<Props> = observer((props) => {
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableGitlabConfig))}
|
||||
onChange={() => {
|
||||
const newEnableGitlabConfig = Boolean(parseInt(enableGitlabConfig)) === true ? "0" : "1";
|
||||
updateConfig("IS_GITLAB_ENABLED", newEnableGitlabConfig);
|
||||
Boolean(parseInt(enableGitlabConfig)) === true
|
||||
? updateConfig("IS_GITLAB_ENABLED", "0")
|
||||
: updateConfig("IS_GITLAB_ENABLED", "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
|
||||
@@ -35,8 +35,9 @@ export const GoogleConfiguration: React.FC<Props> = observer((props) => {
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableGoogleConfig))}
|
||||
onChange={() => {
|
||||
const newEnableGoogleConfig = Boolean(parseInt(enableGoogleConfig)) === true ? "0" : "1";
|
||||
updateConfig("IS_GOOGLE_ENABLED", newEnableGoogleConfig);
|
||||
Boolean(parseInt(enableGoogleConfig)) === true
|
||||
? updateConfig("IS_GOOGLE_ENABLED", "0")
|
||||
: updateConfig("IS_GOOGLE_ENABLED", "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
|
||||
@@ -25,8 +25,9 @@ export const PasswordLoginConfiguration: React.FC<Props> = observer((props) => {
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableEmailPassword))}
|
||||
onChange={() => {
|
||||
const newEnableEmailPassword = Boolean(parseInt(enableEmailPassword)) === true ? "0" : "1";
|
||||
updateConfig("ENABLE_EMAIL_PASSWORD", newEnableEmailPassword);
|
||||
Boolean(parseInt(enableEmailPassword)) === true
|
||||
? updateConfig("ENABLE_EMAIL_PASSWORD", "0")
|
||||
: updateConfig("ENABLE_EMAIL_PASSWORD", "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
|
||||
@@ -209,7 +209,7 @@ export class InstanceStore implements IInstanceStore {
|
||||
});
|
||||
});
|
||||
await this.instanceService.disableEmail();
|
||||
} catch (_error) {
|
||||
} catch (error) {
|
||||
console.error("Error disabling the email");
|
||||
this.instanceConfigurations = instanceConfigurations;
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
FROM python:3.12.10-alpine
|
||||
|
||||
# set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
ENV INSTANCE_CHANGELOG_URL=https://sites.plane.so/pages/691ef037bcfe416a902e48cb55f59891/
|
||||
|
||||
# Update system packages for security
|
||||
RUN apk update && apk upgrade
|
||||
|
||||
WORKDIR /code
|
||||
|
||||
RUN apk add --no-cache --upgrade \
|
||||
"libpq" \
|
||||
"libxslt" \
|
||||
"xmlsec" \
|
||||
"ca-certificates" \
|
||||
"openssl"
|
||||
|
||||
COPY requirements.txt ./
|
||||
COPY requirements ./requirements
|
||||
RUN apk add --no-cache libffi-dev
|
||||
RUN apk add --no-cache --virtual .build-deps \
|
||||
"bash~=5.2" \
|
||||
"g++" \
|
||||
"gcc" \
|
||||
"cargo" \
|
||||
"git" \
|
||||
"make" \
|
||||
"postgresql-dev" \
|
||||
"libc-dev" \
|
||||
"linux-headers" \
|
||||
&& \
|
||||
pip install -r requirements.txt --compile --no-cache-dir \
|
||||
&& \
|
||||
apk del .build-deps \
|
||||
&& \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
|
||||
# Add in Django deps and generate Django's static files
|
||||
COPY manage.py manage.py
|
||||
COPY plane plane/
|
||||
COPY templates templates/
|
||||
COPY package.json package.json
|
||||
|
||||
RUN apk --no-cache add "bash~=5.2"
|
||||
COPY ./bin ./bin/
|
||||
|
||||
RUN mkdir -p /code/plane/logs
|
||||
RUN chmod +x ./bin/*
|
||||
RUN chmod -R 777 /code
|
||||
|
||||
# Expose container port and run entry point script
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["./bin/docker-entrypoint-api.sh"]
|
||||
@@ -1,46 +0,0 @@
|
||||
FROM python:3.12.5-alpine AS backend
|
||||
|
||||
# set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
ENV INSTANCE_CHANGELOG_URL https://sites.plane.so/pages/691ef037bcfe416a902e48cb55f59891/
|
||||
|
||||
RUN apk --no-cache add \
|
||||
"bash~=5.2" \
|
||||
"libpq" \
|
||||
"libxslt" \
|
||||
"nodejs-current" \
|
||||
"xmlsec" \
|
||||
"libffi-dev" \
|
||||
"bash~=5.2" \
|
||||
"g++" \
|
||||
"gcc" \
|
||||
"cargo" \
|
||||
"git" \
|
||||
"make" \
|
||||
"postgresql-dev" \
|
||||
"libc-dev" \
|
||||
"linux-headers"
|
||||
|
||||
WORKDIR /code
|
||||
|
||||
COPY requirements.txt ./requirements.txt
|
||||
ADD requirements ./requirements
|
||||
|
||||
# Install the local development settings
|
||||
RUN pip install -r requirements/local.txt --compile --no-cache-dir
|
||||
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN mkdir -p /code/plane/logs
|
||||
RUN chmod -R +x /code/bin
|
||||
RUN chmod -R 777 /code
|
||||
|
||||
|
||||
# Expose container port and run entry point script
|
||||
EXPOSE 8000
|
||||
|
||||
CMD [ "./bin/docker-entrypoint-api-local.sh" ]
|
||||
|
||||
@@ -465,7 +465,3 @@ if ENABLE_DRF_SPECTACULAR:
|
||||
REST_FRAMEWORK["DEFAULT_SCHEMA_CLASS"] = "drf_spectacular.openapi.AutoSchema"
|
||||
INSTALLED_APPS.append("drf_spectacular")
|
||||
from .openapi import SPECTACULAR_SETTINGS # noqa: F401
|
||||
|
||||
# MongoDB Settings
|
||||
MONGO_DB_URL = os.environ.get("MONGO_DB_URL", False)
|
||||
MONGO_DB_DATABASE = os.environ.get("MONGO_DB_DATABASE", False)
|
||||
|
||||
@@ -118,7 +118,4 @@ class MongoConnection:
|
||||
Returns:
|
||||
bool: True if MongoDB is configured and connected, False otherwise
|
||||
"""
|
||||
|
||||
if cls._client is None:
|
||||
cls._instance = cls()
|
||||
return cls._client is not None and cls._db is not None
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/server.js"],
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": ["@plane/eslint-config/server.js"],
|
||||
"parser": "@typescript-eslint/parser"
|
||||
}
|
||||
@@ -1,17 +1,20 @@
|
||||
// Third-party libraries
|
||||
import { Redis } from "ioredis";
|
||||
// Hocuspocus extensions and core
|
||||
import { Database } from "@hocuspocus/extension-database";
|
||||
import { Extension } from "@hocuspocus/server";
|
||||
import { Logger } from "@hocuspocus/extension-logger";
|
||||
import { Redis as HocusPocusRedis } from "@hocuspocus/extension-redis";
|
||||
import { Extension } from "@hocuspocus/server";
|
||||
import { Redis } from "ioredis";
|
||||
// core helpers and utilities
|
||||
import { manualLogger } from "@/core/helpers/logger.js";
|
||||
import { getRedisUrl } from "@/core/lib/utils/redis-url.js";
|
||||
// core libraries
|
||||
import { fetchPageDescriptionBinary, updatePageDescription } from "@/core/lib/page.js";
|
||||
import { getRedisUrl } from "@/core/lib/utils/redis-url.js";
|
||||
import { type HocusPocusServerContext, type TDocumentTypes } from "@/core/types/common.js";
|
||||
// plane live libraries
|
||||
import { fetchDocument } from "@/plane-live/lib/fetch-document.js";
|
||||
import { updateDocument } from "@/plane-live/lib/update-document.js";
|
||||
// types
|
||||
import { type HocusPocusServerContext, type TDocumentTypes } from "@/core/types/common.js";
|
||||
|
||||
export const getExtensions: () => Promise<Extension[]> = async () => {
|
||||
const extensions: Extension[] = [
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Server } from "@hocuspocus/server";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
// editor types
|
||||
import { TUserDetails } from "@plane/editor";
|
||||
import { DocumentCollaborativeEvents, TDocumentEventsServer } from "@plane/editor/lib";
|
||||
// extensions
|
||||
import { getExtensions } from "@/core/extensions/index.js";
|
||||
// lib
|
||||
import { handleAuthentication } from "@/core/lib/authentication.js";
|
||||
// extensions
|
||||
import { getExtensions } from "@/core/extensions/index.js";
|
||||
import { DocumentCollaborativeEvents, TDocumentEventsServer } from "@plane/editor/lib";
|
||||
// editor types
|
||||
import { TUserDetails } from "@plane/editor";
|
||||
// types
|
||||
import { type HocusPocusServerContext } from "@/core/types/common.js";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// core helpers
|
||||
import { manualLogger } from "@/core/helpers/logger.js";
|
||||
// services
|
||||
import { UserService } from "@/core/services/user.service.js";
|
||||
// core helpers
|
||||
import { manualLogger } from "@/core/helpers/logger.js";
|
||||
|
||||
const userService = new UserService();
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import compression from "compression";
|
||||
import cors from "cors";
|
||||
import express, { Request, Response } from "express";
|
||||
import expressWs from "express-ws";
|
||||
import express, { Request, Response } from "express";
|
||||
import helmet from "helmet";
|
||||
// hocuspocus server
|
||||
import { getHocusPocusServer } from "@/core/hocuspocus-server.js";
|
||||
// helpers
|
||||
import { convertHTMLDocumentToAllFormats } from "@/core/helpers/convert-document.js";
|
||||
import { logger, manualLogger } from "@/core/helpers/logger.js";
|
||||
import { getHocusPocusServer } from "@/core/hocuspocus-server.js";
|
||||
// types
|
||||
import { TConvertDocumentRequestBody } from "@/core/types/common.js";
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
.next/*
|
||||
out/*
|
||||
public/*
|
||||
dist/*
|
||||
node_modules/*
|
||||
.turbo/*
|
||||
.env*
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.production
|
||||
.env.test
|
||||
@@ -1,4 +1,6 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/next.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
FROM node:22-alpine
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN corepack enable pnpm && pnpm add -g turbo
|
||||
RUN pnpm install
|
||||
|
||||
EXPOSE 3002
|
||||
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
||||
|
||||
VOLUME [ "/app/node_modules", "/app/apps/space/node_modules"]
|
||||
|
||||
CMD ["pnpm", "dev", "--filter=space"]
|
||||
@@ -1,103 +0,0 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
FROM node:22-alpine AS base
|
||||
|
||||
# Setup pnpm package manager with corepack and configure global bin directory for caching
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 1: Build the project
|
||||
# *****************************************************************************
|
||||
FROM base AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
ARG TURBO_VERSION=2.5.6
|
||||
RUN corepack enable pnpm && pnpm add -g turbo@${TURBO_VERSION}
|
||||
COPY . .
|
||||
|
||||
RUN turbo prune --scope=space --docker
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 2: Install dependencies & build the project
|
||||
# *****************************************************************************
|
||||
FROM base AS installer
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=builder /app/out/json/ .
|
||||
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||
RUN corepack enable pnpm
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/pnpm/store
|
||||
|
||||
COPY --from=builder /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_WEB_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV TURBO_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN pnpm turbo run build --filter=space
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 3: Copy the project and start it
|
||||
# *****************************************************************************
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
# Don't run production as root
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
USER nextjs
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=installer /app/apps/space/.next/standalone ./
|
||||
COPY --from=installer /app/apps/space/.next/static ./apps/space/.next/static
|
||||
COPY --from=installer /app/apps/space/public ./apps/space/public
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_WEB_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV TURBO_TELEMETRY_DISABLED=1
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "apps/space/server.js"]
|
||||
+1
-10
@@ -1,13 +1,4 @@
|
||||
.next/*
|
||||
out/*
|
||||
public/*
|
||||
core/local-db/worker/wa-sqlite/src/*
|
||||
dist/*
|
||||
node_modules/*
|
||||
.turbo/*
|
||||
.env*
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.production
|
||||
.env.test
|
||||
core/local-db/worker/wa-sqlite/src/*
|
||||
@@ -1,4 +1,6 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/next.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
FROM node:22-alpine
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
RUN corepack enable pnpm && pnpm add -g turbo
|
||||
RUN pnpm install
|
||||
|
||||
EXPOSE 3000
|
||||
VOLUME [ "/app/node_modules", "/app/web/node_modules" ]
|
||||
CMD ["pnpm", "dev", "--filter=web"]
|
||||
@@ -1,120 +0,0 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
FROM node:22-alpine AS base
|
||||
|
||||
# Setup pnpm package manager with corepack and configure global bin directory for caching
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 1: Build the project
|
||||
# *****************************************************************************
|
||||
FROM base AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
ARG TURBO_VERSION=2.5.6
|
||||
RUN corepack enable pnpm && pnpm add -g turbo@${TURBO_VERSION}
|
||||
COPY . .
|
||||
|
||||
RUN turbo prune --scope=web --docker
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 2: Install dependencies & build the project
|
||||
# *****************************************************************************
|
||||
# Add lockfile and package.json's of isolated subworkspace
|
||||
FROM base AS installer
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
# First install the dependencies (as they change less often)
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=builder /app/out/json/ .
|
||||
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||
RUN corepack enable pnpm
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/pnpm/store
|
||||
|
||||
# Build the project
|
||||
COPY --from=builder /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_LIVE_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_LIVE_BASE_URL=$NEXT_PUBLIC_LIVE_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_LIVE_BASE_PATH="/live"
|
||||
ENV NEXT_PUBLIC_LIVE_BASE_PATH=$NEXT_PUBLIC_LIVE_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_WEB_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV TURBO_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN pnpm turbo run build --filter=web
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 3: Copy the project and start it
|
||||
# *****************************************************************************
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
# Don't run production as root
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
USER nextjs
|
||||
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=installer /app/apps/web/.next/standalone ./
|
||||
COPY --from=installer /app/apps/web/.next/static ./apps/web/.next/static
|
||||
COPY --from=installer /app/apps/web/public ./apps/web/public
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_LIVE_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_LIVE_BASE_URL=$NEXT_PUBLIC_LIVE_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_LIVE_BASE_PATH="/live"
|
||||
ENV NEXT_PUBLIC_LIVE_BASE_PATH=$NEXT_PUBLIC_LIVE_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_WEB_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV TURBO_TELEMETRY_DISABLED=1
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "apps/web/server.js"]
|
||||
@@ -1,24 +1,36 @@
|
||||
import { FC } from "react";
|
||||
import { FC, useEffect, useRef } from "react";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import { observer } from "mobx-react";
|
||||
// plane helpers
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
// components
|
||||
import { SidebarWrapper } from "@/components/sidebar/sidebar-wrapper";
|
||||
import { AppSidebarToggleButton } from "@/components/sidebar/sidebar-toggle-button";
|
||||
import { SidebarDropdown } from "@/components/workspace/sidebar/dropdown";
|
||||
import { SidebarFavoritesMenu } from "@/components/workspace/sidebar/favorites/favorites-menu";
|
||||
import { HelpMenu } from "@/components/workspace/sidebar/help-menu";
|
||||
import { SidebarProjectsList } from "@/components/workspace/sidebar/projects-list";
|
||||
import { SidebarQuickActions } from "@/components/workspace/sidebar/quick-actions";
|
||||
import { SidebarMenuItems } from "@/components/workspace/sidebar/sidebar-menu-items";
|
||||
// hooks
|
||||
import { useAppTheme } from "@/hooks/store/use-app-theme";
|
||||
import { useFavorite } from "@/hooks/store/use-favorite";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useAppRail } from "@/hooks/use-app-rail";
|
||||
import useSize from "@/hooks/use-window-size";
|
||||
// plane web components
|
||||
import { WorkspaceEditionBadge } from "@/plane-web/components/workspace/edition-badge";
|
||||
import { SidebarTeamsList } from "@/plane-web/components/workspace/sidebar/teams-sidebar-list";
|
||||
|
||||
export const AppSidebar: FC = observer(() => {
|
||||
// store hooks
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
|
||||
const { shouldRenderAppRail, isEnabled: isAppRailEnabled } = useAppRail();
|
||||
const { groupedFavorites } = useFavorite();
|
||||
const windowSize = useSize();
|
||||
// refs
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
// derived values
|
||||
const canPerformWorkspaceMemberActions = allowPermissions(
|
||||
@@ -26,17 +38,55 @@ export const AppSidebar: FC = observer(() => {
|
||||
EUserPermissionsLevel.WORKSPACE
|
||||
);
|
||||
|
||||
useOutsideClickDetector(ref, () => {
|
||||
if (sidebarCollapsed === false) {
|
||||
if (window.innerWidth < 768) {
|
||||
toggleSidebar();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (windowSize[0] < 768 && !sidebarCollapsed) toggleSidebar();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [windowSize]);
|
||||
|
||||
const isFavoriteEmpty = isEmpty(groupedFavorites);
|
||||
|
||||
return (
|
||||
<SidebarWrapper title="Projects" quickActions={<SidebarQuickActions />}>
|
||||
<SidebarMenuItems />
|
||||
{/* Favorites Menu */}
|
||||
{canPerformWorkspaceMemberActions && !isFavoriteEmpty && <SidebarFavoritesMenu />}
|
||||
{/* Teams List */}
|
||||
<SidebarTeamsList />
|
||||
{/* Projects List */}
|
||||
<SidebarProjectsList />
|
||||
</SidebarWrapper>
|
||||
<>
|
||||
<div className="flex flex-col gap-3 px-3">
|
||||
{/* Workspace switcher and settings */}
|
||||
{!shouldRenderAppRail && <SidebarDropdown />}
|
||||
|
||||
{isAppRailEnabled && (
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-md text-custom-text-200 font-medium pt-1">Projects</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<AppSidebarToggleButton />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Quick actions */}
|
||||
<SidebarQuickActions />
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 overflow-x-hidden scrollbar-sm h-full w-full overflow-y-auto vertical-scrollbar px-3 pt-3 pb-0.5">
|
||||
<SidebarMenuItems />
|
||||
{/* Favorites Menu */}
|
||||
{canPerformWorkspaceMemberActions && !isFavoriteEmpty && <SidebarFavoritesMenu />}
|
||||
{/* Teams List */}
|
||||
<SidebarTeamsList />
|
||||
{/* Projects List */}
|
||||
<SidebarProjectsList />
|
||||
</div>
|
||||
{/* Help Section */}
|
||||
<div className="flex items-center justify-between p-3 border-t border-custom-border-200 bg-custom-sidebar-background-100 h-12">
|
||||
<WorkspaceEditionBadge />
|
||||
<div className="flex items-center gap-2">
|
||||
{!shouldRenderAppRail && <HelpMenu />}
|
||||
{!isAppRailEnabled && <AppSidebarToggleButton />}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -80,7 +80,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
||||
const payload: any = { ...data };
|
||||
if (data.sort_order) payload.sort_order = data.sort_order.newSortOrder;
|
||||
|
||||
await updateIssue?.(issue.project_id, issue.id, payload);
|
||||
updateIssue && (await updateIssue(issue.project_id, issue.id, payload));
|
||||
};
|
||||
|
||||
const isAllowed = allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT);
|
||||
|
||||
@@ -112,8 +112,8 @@ export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
|
||||
const issueDetails = getIssueById(issueId);
|
||||
const projectIdentifier = getProjectIdentifierById(issueDetails?.project_id);
|
||||
|
||||
const handleIssuePeekOverview = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
e.stopPropagation();
|
||||
const handleIssuePeekOverview = (e: any) => {
|
||||
e.stopPropagation(true);
|
||||
e.preventDefault();
|
||||
handleRedirection(workspaceSlug, issueDetails, isMobile);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-sc
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { EIssueFilterType, EUserPermissions, EUserPermissionsLevel, WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
|
||||
import { EIssueServiceType, EIssuesStoreType, EIssueLayoutTypes, TIssue } from "@plane/types";
|
||||
import { EIssueServiceType, EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
|
||||
//constants
|
||||
//hooks
|
||||
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
@@ -42,7 +42,7 @@ export type KanbanStoreType =
|
||||
|
||||
export interface IBaseKanBanLayout {
|
||||
QuickActions: FC<IQuickActionProps>;
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
||||
canEditPropertiesBasedOnProject?: (projectId: string) => boolean;
|
||||
isCompletedCycle?: boolean;
|
||||
viewId?: string | undefined;
|
||||
|
||||
@@ -238,7 +238,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [issue?.id, isDragAllowed, canDropOverIssue, setIsCurrentBlockDragging, setIsDraggingOverBlock]);
|
||||
}, [cardRef?.current, issue?.id, isDragAllowed, canDropOverIssue, setIsCurrentBlockDragging, setIsDraggingOverBlock]);
|
||||
|
||||
if (!issue) return null;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { MutableRefObject } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import {
|
||||
GroupByColumnTypes,
|
||||
IGroupByColumn,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
TIssueGroupByOptions,
|
||||
TIssueOrderByOptions,
|
||||
} from "@plane/types";
|
||||
// constants
|
||||
import { ContentWrapper } from "@plane/ui";
|
||||
// components
|
||||
import RenderIfVisible from "@/components/core/render-if-visible-HOC";
|
||||
@@ -90,6 +92,8 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
subGroupIndex = 0,
|
||||
isEpic = false,
|
||||
} = props;
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const storeType = useIssueStoreType();
|
||||
const issueKanBanView = useKanbanView();
|
||||
|
||||
@@ -78,7 +78,7 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
|
||||
title: "Success!",
|
||||
message: "Work items added to the cycle successfully.",
|
||||
});
|
||||
} catch {
|
||||
} catch (error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
|
||||
@@ -7,7 +7,7 @@ import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||
import { IssueBlockRoot } from "./block-root";
|
||||
import { TRenderQuickActions } from "./list-view-types";
|
||||
|
||||
type Props = {
|
||||
interface Props {
|
||||
issueIds: TGroupedIssues | any;
|
||||
issuesMap: TIssueMap;
|
||||
groupId: string;
|
||||
@@ -20,7 +20,7 @@ type Props = {
|
||||
canDropOverIssue: boolean;
|
||||
selectionHelpers: TSelectionHelper;
|
||||
isEpic?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const IssueBlocksList: FC<Props> = (props) => {
|
||||
const {
|
||||
|
||||
@@ -81,7 +81,7 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
|
||||
title: "Success!",
|
||||
message: "Work items added to the cycle successfully.",
|
||||
});
|
||||
} catch {
|
||||
} catch (error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
|
||||
@@ -192,9 +192,7 @@ export const ListGroup = observer((props: Props) => {
|
||||
const sourceGroupId = source?.data?.groupId as string | undefined;
|
||||
const currentGroupId = group.id;
|
||||
|
||||
if (sourceGroupId) {
|
||||
handleWorkFlowState(sourceGroupId, currentGroupId);
|
||||
}
|
||||
sourceGroupId && handleWorkFlowState(sourceGroupId, currentGroupId);
|
||||
|
||||
const sourceIndex = getGroupIndex(sourceGroupId);
|
||||
const currentIndex = getGroupIndex(currentGroupId);
|
||||
@@ -232,7 +230,15 @@ export const ListGroup = observer((props: Props) => {
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [group, orderBy, getGroupIndex, setDragColumnOrientation, setIsDraggingOverColumn, isWorkflowDropDisabled]);
|
||||
}, [
|
||||
groupRef?.current,
|
||||
group,
|
||||
orderBy,
|
||||
getGroupIndex,
|
||||
setDragColumnOrientation,
|
||||
setIsDraggingOverColumn,
|
||||
isWorkflowDropDisabled,
|
||||
]);
|
||||
|
||||
const isDragAllowed = !!group_by && DRAG_ALLOWED_GROUPS.includes(group_by);
|
||||
const canOverlayBeVisible = isWorkflowDropDisabled || orderBy !== "sort_order" || !!group.isDropDisabled;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { FC, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { Link2, type LucideIcon, MoveDiagonal, MoveRight } from "lucide-react";
|
||||
import { Link2, MoveDiagonal, MoveRight } from "lucide-react";
|
||||
// plane imports
|
||||
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
CenterPanelIcon,
|
||||
CustomSelect,
|
||||
FullScreenPanelIcon,
|
||||
type ISvgIcons,
|
||||
SidePanelIcon,
|
||||
TOAST_TYPE,
|
||||
Tooltip,
|
||||
@@ -34,7 +33,7 @@ import { NameDescriptionUpdateStatus } from "../issue-update-status";
|
||||
|
||||
export type TPeekModes = "side-peek" | "modal" | "full-screen";
|
||||
|
||||
const PEEK_OPTIONS: { key: TPeekModes; icon: LucideIcon | React.FC<ISvgIcons>; i18n_title: string }[] = [
|
||||
const PEEK_OPTIONS: { key: TPeekModes; icon: any; i18n_title: string }[] = [
|
||||
{
|
||||
key: "side-peek",
|
||||
icon: SidePanelIcon,
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// types
|
||||
import { PROJECT_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
|
||||
import { PROJECT_SETTINGS_TRACKER_ELEMENTS, PROJECT_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
|
||||
import type { IIssueLabel } from "@plane/types";
|
||||
// ui
|
||||
import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
|
||||
@@ -154,7 +154,7 @@ export const LabelDndHOC = observer((props: Props) => {
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [dragHandleRef?.current, label, isChild, isGroup, isLastChild, onDrop]);
|
||||
}, [labelRef?.current, dragHandleRef?.current, label, isChild, isGroup, isLastChild, onDrop]);
|
||||
|
||||
const isMakeChild = instruction == "make-child";
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ export const ModuleAnalyticsProgress: FC<TModuleAnalyticsProgress> = observer((p
|
||||
await fetchModuleDetails(workspaceSlug, projectId, moduleId);
|
||||
}
|
||||
setLoader(false);
|
||||
} catch {
|
||||
} catch (error) {
|
||||
setLoader(false);
|
||||
setPlotType(moduleId, plotType);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from "@plane/constants";
|
||||
// plane types
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ILinkDetails, IModule, ModuleLink, TModuleStatus } from "@plane/types";
|
||||
import { ILinkDetails, IModule, ModuleLink } from "@plane/types";
|
||||
// plane ui
|
||||
import { Loader, LayersIcon, CustomSelect, ModuleStatusIcon, TOAST_TYPE, setToast, TextArea } from "@plane/ui";
|
||||
// components
|
||||
@@ -270,7 +270,7 @@ export const ModuleAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
||||
</span>
|
||||
}
|
||||
value={value}
|
||||
onChange={(value: TModuleStatus) => {
|
||||
onChange={(value: any) => {
|
||||
submitChanges({ status: value });
|
||||
}}
|
||||
disabled={!isEditingAllowed || isArchived}
|
||||
|
||||
@@ -27,7 +27,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const CreateOrJoinWorkspaces: React.FC<Props> = observer((props) => {
|
||||
const { invitations, totalSteps: _totalSteps, stepChange, finishOnboarding } = props;
|
||||
const { invitations, totalSteps, stepChange, finishOnboarding } = props;
|
||||
// states
|
||||
const [currentView, setCurrentView] = useState<ECreateOrJoinWorkspaceViews | null>(null);
|
||||
// store hooks
|
||||
|
||||
@@ -262,7 +262,7 @@ const InviteMemberInput: React.FC<InviteMemberFormProps> = observer((props) => {
|
||||
});
|
||||
|
||||
export const InviteMembers: React.FC<Props> = (props) => {
|
||||
const { finishOnboarding, totalSteps: _totalSteps, workspace } = props;
|
||||
const { finishOnboarding, totalSteps, workspace } = props;
|
||||
|
||||
const [isInvitationDisabled, setIsInvitationDisabled] = useState(true);
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import { Listbox } from "@headlessui/react";
|
||||
import { ROLE, ROLE_DETAILS, EUserPermissions, MEMBER_TRACKER_EVENTS, MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { EOnboardingSteps } from "@plane/types";
|
||||
import { EOnboardingSteps, IWorkspace } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// constants
|
||||
@@ -30,6 +30,7 @@ import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// hooks
|
||||
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
import { useUser, useUserProfile } from "@/hooks/store/user";
|
||||
// services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// components
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { FileText, Layers, type LucideIcon } from "lucide-react";
|
||||
// plane imports
|
||||
import { ContrastIcon, DiceIcon, type ISvgIcons, LayersIcon } from "@plane/ui";
|
||||
// local imports
|
||||
import type { TTourSteps } from "./root";
|
||||
// icons
|
||||
import { FileText, Layers } from "lucide-react";
|
||||
import { ContrastIcon, DiceIcon, LayersIcon } from "@plane/ui";
|
||||
// types
|
||||
import { TTourSteps } from "./root";
|
||||
|
||||
const sidebarOptions: {
|
||||
key: TTourSteps;
|
||||
label: string;
|
||||
Icon: LucideIcon | React.FC<ISvgIcons>;
|
||||
Icon: any;
|
||||
}[] = [
|
||||
{
|
||||
key: "work-items",
|
||||
|
||||
@@ -27,7 +27,7 @@ import { PageEditorToolbarRoot } from "./toolbar";
|
||||
export type TPageRootHandlers = {
|
||||
create: (payload: Partial<TPage>) => Promise<Partial<TPage> | undefined>;
|
||||
fetchAllVersions: (pageId: string) => Promise<TPageVersion[] | undefined>;
|
||||
fetchDescriptionBinary: () => Promise<ArrayBuffer>;
|
||||
fetchDescriptionBinary: () => Promise<any>;
|
||||
fetchVersionDetails: (pageId: string, versionId: string) => Promise<TPageVersion | undefined>;
|
||||
getRedirectionLink: (pageId: string) => string;
|
||||
updateDescription: (document: TDocumentPayload) => Promise<void>;
|
||||
|
||||
@@ -77,7 +77,7 @@ export const PageForm: React.FC<Props> = (props) => {
|
||||
</>
|
||||
</span>
|
||||
}
|
||||
onChange={(val) => {
|
||||
onChange={(val: any) => {
|
||||
let logoValue = {};
|
||||
|
||||
if (val?.type === "emoji")
|
||||
|
||||
@@ -56,7 +56,7 @@ export const ProfileActivityListPage: React.FC<Props> = observer((props) => {
|
||||
<>
|
||||
{userProfileActivity ? (
|
||||
<ul role="list">
|
||||
{userProfileActivity.results.map((activityItem) => {
|
||||
{userProfileActivity.results.map((activityItem: any) => {
|
||||
if (activityItem.field === "comment")
|
||||
return (
|
||||
<div key={activityItem.id} className="mt-2">
|
||||
@@ -98,7 +98,7 @@ export const ProfileActivityListPage: React.FC<Props> = observer((props) => {
|
||||
editable={false}
|
||||
id={activityItem.id}
|
||||
initialValue={
|
||||
(activityItem?.new_value !== "" ? activityItem.new_value : activityItem.old_value) ?? ""
|
||||
activityItem?.new_value !== "" ? activityItem.new_value : activityItem.old_value
|
||||
}
|
||||
containerClassName="text-xs bg-custom-background-100"
|
||||
workspaceId={activityItem?.workspace_detail?.id?.toString() ?? ""}
|
||||
@@ -155,7 +155,7 @@ export const ProfileActivityListPage: React.FC<Props> = observer((props) => {
|
||||
<span className="text-gray font-medium">{activityItem.actor_detail.first_name} Bot</span>
|
||||
) : (
|
||||
<Link
|
||||
href={`/${activityItem.workspace_detail?.slug}/profile/${activityItem.actor_detail.id}`}
|
||||
href={`/${activityItem.workspace_detail.slug}/profile/${activityItem.actor_detail.id}`}
|
||||
className="inline"
|
||||
>
|
||||
<span className="text-gray font-medium">
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import { FC, useEffect, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane helpers
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
// components
|
||||
import { AppSidebarToggleButton } from "@/components/sidebar/sidebar-toggle-button";
|
||||
import { SidebarDropdown } from "@/components/workspace/sidebar/dropdown";
|
||||
import { HelpMenu } from "@/components/workspace/sidebar/help-menu";
|
||||
// hooks
|
||||
import { useAppTheme } from "@/hooks/store/use-app-theme";
|
||||
import { useAppRail } from "@/hooks/use-app-rail";
|
||||
import useSize from "@/hooks/use-window-size";
|
||||
// plane web components
|
||||
import { WorkspaceEditionBadge } from "@/plane-web/components/workspace/edition-badge";
|
||||
|
||||
type TSidebarWrapperProps = {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
quickActions?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const SidebarWrapper: FC<TSidebarWrapperProps> = observer((props) => {
|
||||
const { children, title, quickActions } = props;
|
||||
// store hooks
|
||||
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
|
||||
const { shouldRenderAppRail, isEnabled: isAppRailEnabled } = useAppRail();
|
||||
const windowSize = useSize();
|
||||
// refs
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useOutsideClickDetector(ref, () => {
|
||||
if (sidebarCollapsed === false && window.innerWidth < 768) {
|
||||
toggleSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (windowSize[0] < 768 && !sidebarCollapsed) toggleSidebar();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [windowSize]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className="flex flex-col h-full w-full">
|
||||
<div className="flex flex-col gap-3 px-3">
|
||||
{/* Workspace switcher and settings */}
|
||||
{!shouldRenderAppRail && <SidebarDropdown />}
|
||||
|
||||
{isAppRailEnabled && (
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-md text-custom-text-200 font-medium pt-1">{title}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<AppSidebarToggleButton />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Quick actions */}
|
||||
{quickActions}
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 overflow-x-hidden scrollbar-sm h-full w-full overflow-y-auto vertical-scrollbar px-3 pt-3 pb-0.5">
|
||||
{children}
|
||||
</div>
|
||||
{/* Help Section */}
|
||||
<div className="flex items-center justify-between p-3 border-t border-custom-border-200 bg-custom-sidebar-background-100 h-12">
|
||||
<WorkspaceEditionBadge />
|
||||
<div className="flex items-center gap-2">
|
||||
{!shouldRenderAppRail && <HelpMenu />}
|
||||
{!isAppRailEnabled && <AppSidebarToggleButton />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
|
||||
import orderBy from "lodash/orderBy";
|
||||
import { IFavorite, InstructionType, IPragmaticPayloadLocation, TDropTarget } from "@plane/types";
|
||||
|
||||
export type TargetData = {
|
||||
|
||||
@@ -66,7 +66,7 @@ export const UserMenuRoot = observer((props: Props) => {
|
||||
|
||||
return (
|
||||
<Menu as="div" className="relative flex-shrink-0">
|
||||
{({ open }: { open: boolean; close: () => void }) => {
|
||||
{({ open, close }: { open: boolean; close: () => void }) => {
|
||||
// Update local state directly
|
||||
if (isUserMenuOpen !== open) {
|
||||
setIsUserMenuOpen(open);
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
List,
|
||||
ListOrdered,
|
||||
ListTodo,
|
||||
type LucideIcon,
|
||||
LucideIcon,
|
||||
Strikethrough,
|
||||
Table,
|
||||
TextQuote,
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
} from "lucide-react";
|
||||
// plane imports
|
||||
import type { TCommandExtraProps, TEditorCommands, TEditorFontStyle } from "@plane/editor";
|
||||
import { type ISvgIcons, MonospaceIcon, SansSerifIcon, SerifIcon } from "@plane/ui";
|
||||
import { MonospaceIcon, SansSerifIcon, SerifIcon } from "@plane/ui";
|
||||
import { convertRemToPixel } from "@plane/utils";
|
||||
|
||||
type TEditorTypes = "lite" | "document" | "sticky";
|
||||
@@ -191,7 +191,7 @@ export const TOOLBAR_ITEMS: {
|
||||
export const EDITOR_FONT_STYLES: {
|
||||
key: TEditorFontStyle;
|
||||
label: string;
|
||||
icon: LucideIcon | React.FC<ISvgIcons>;
|
||||
icon: any;
|
||||
}[] = [
|
||||
{
|
||||
key: "sans-serif",
|
||||
|
||||
@@ -90,7 +90,7 @@ export const useGroupIssuesDragNDrop = (
|
||||
delete data[moduleKey];
|
||||
}
|
||||
|
||||
updateIssue?.(projectId, issueId, data).catch(() => setToast(errorToastProps));
|
||||
updateIssue && updateIssue(projectId, issueId, data).catch(() => setToast(errorToastProps));
|
||||
};
|
||||
|
||||
const handleOnDrop = async (source: GroupDropLocation, destination: GroupDropLocation) => {
|
||||
|
||||
@@ -18,7 +18,7 @@ export const useIntersectionObserver = (
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[entries.length - 1].isIntersecting) {
|
||||
callback?.();
|
||||
callback && callback();
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
export const getValueFromLocalStorage = (key: string, defaultValue: unknown) => {
|
||||
export const getValueFromLocalStorage = (key: string, defaultValue: any) => {
|
||||
if (typeof window === undefined || typeof window === "undefined") return defaultValue;
|
||||
try {
|
||||
const item = window.localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : defaultValue;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
window.localStorage.removeItem(key);
|
||||
return defaultValue;
|
||||
}
|
||||
};
|
||||
|
||||
export const setValueIntoLocalStorage = (key: string, value: unknown) => {
|
||||
export const setValueIntoLocalStorage = (key: string, value: any) => {
|
||||
if (typeof window === undefined || typeof window === "undefined") return false;
|
||||
try {
|
||||
window.localStorage.setItem(key, JSON.stringify(value));
|
||||
return true;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ const useReloadConfirmations = (isActive = true, message?: string, defaultShowAl
|
||||
// show confirm dialog
|
||||
const isLeaving = confirm(alertMessage);
|
||||
if (isLeaving) {
|
||||
onLeave?.();
|
||||
onLeave && onLeave();
|
||||
} else {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
@@ -18,7 +18,6 @@ import { runQuery } from "./utils/query-executor";
|
||||
import { sanitizeWorkItemQueries } from "./utils/query-sanitizer.ts";
|
||||
import { createTables } from "./utils/tables";
|
||||
import { clearOPFS, getGroupedIssueResults, getSubGroupedIssueResults, log, logError } from "./utils/utils";
|
||||
import type { DBClass } from "./worker/db";
|
||||
|
||||
const DB_VERSION = 1.3;
|
||||
const PAGE_SIZE = 500;
|
||||
@@ -71,6 +70,7 @@ export class Storage {
|
||||
};
|
||||
|
||||
private initializeWorker = async (workspaceSlug: string) => {
|
||||
const { DBClass } = await import("./worker/db");
|
||||
const worker = new Worker(new URL("./worker/db.ts", import.meta.url));
|
||||
const MyWorker = Comlink.wrap<typeof DBClass>(worker);
|
||||
|
||||
@@ -367,6 +367,7 @@ export class Storage {
|
||||
issueResults = getGroupedIssueResults(issueResults);
|
||||
}
|
||||
}
|
||||
const groupCount = group_by ? Object.keys(issueResults).length : undefined;
|
||||
// const subGroupCount = sub_group_by ? Object.keys(issueResults[Object.keys(issueResults)[0]]).length : undefined;
|
||||
const groupingEnd = performance.now();
|
||||
|
||||
|
||||
@@ -98,11 +98,9 @@ export class UserSettingsStore implements IUserSettingsStore {
|
||||
} else if (workspaceSlug) {
|
||||
await persistence.initialize(workspaceSlug);
|
||||
persistence.syncWorkspace();
|
||||
if (projectId) {
|
||||
persistence.syncIssues(projectId);
|
||||
}
|
||||
projectId && persistence.syncIssues(projectId);
|
||||
}
|
||||
} catch {
|
||||
} catch (e) {
|
||||
console.warn("error while toggling local DB");
|
||||
runInAction(() => {
|
||||
this.canUseLocalDB = currentLocalDBValue;
|
||||
|
||||
@@ -97,7 +97,7 @@ priority=20
|
||||
|
||||
[program:live]
|
||||
directory=/app/live
|
||||
command=sh -c "node live/server.js"
|
||||
command=sh -c "node /app/live/apps/live/dist/server.js"
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/app/logs/access/live.log
|
||||
@@ -106,7 +106,7 @@ stderr_logfile=/app/logs/error/live.err.log
|
||||
# stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=50MB
|
||||
stderr_logfile_backups=5
|
||||
environment=PORT=3005,HOSTNAME=0.0.0.0
|
||||
environment=PORT=3005,HOSTNAME=0.0.0.0,LIVE_BASE_PATH=/live
|
||||
priority=20
|
||||
|
||||
|
||||
@@ -121,4 +121,4 @@ stderr_logfile=/app/logs/error/proxy.err.log
|
||||
# stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=50MB
|
||||
stderr_logfile_backups=5
|
||||
priority=20
|
||||
priority=20
|
||||
|
||||
@@ -3,19 +3,25 @@ services:
|
||||
image: ${DOCKERHUB_USER:-local}/plane-frontend:${APP_RELEASE:-latest}
|
||||
build:
|
||||
context: ../../
|
||||
dockerfile: apps/web/Dockerfile.web
|
||||
dockerfile: Dockerfile.node
|
||||
args:
|
||||
APP_SCOPE: web
|
||||
|
||||
space:
|
||||
image: ${DOCKERHUB_USER:-local}/plane-space:${APP_RELEASE:-latest}
|
||||
build:
|
||||
context: ../../
|
||||
dockerfile: apps/space/Dockerfile.space
|
||||
dockerfile: Dockerfile.node
|
||||
args:
|
||||
APP_SCOPE: space
|
||||
|
||||
admin:
|
||||
image: ${DOCKERHUB_USER:-local}/plane-admin:${APP_RELEASE:-latest}
|
||||
build:
|
||||
context: ../../
|
||||
dockerfile: apps/admin/Dockerfile.admin
|
||||
dockerfile: Dockerfile.node
|
||||
args:
|
||||
APP_SCOPE: admin
|
||||
|
||||
live:
|
||||
image: ${DOCKERHUB_USER:-local}/plane-live:${APP_RELEASE:-latest}
|
||||
@@ -26,7 +32,7 @@ services:
|
||||
api:
|
||||
image: ${DOCKERHUB_USER:-local}/plane-backend:${APP_RELEASE:-latest}
|
||||
build:
|
||||
context: ../../apps/api
|
||||
context: ../../
|
||||
dockerfile: Dockerfile.api
|
||||
|
||||
proxy:
|
||||
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
# docker-bake.hcl
|
||||
#
|
||||
# Build all runtime images for Plane services and the AIO assembler image.
|
||||
# Uses unified Dockerfiles under plane/ for Node (Next.js) apps and API.
|
||||
#
|
||||
# Usage examples:
|
||||
# # Build all individual runtime images (no push)
|
||||
# docker buildx bake
|
||||
#
|
||||
# # Build everything including AIO
|
||||
# docker buildx bake all
|
||||
#
|
||||
# # Build and push with custom tag and registry/prefix
|
||||
# docker buildx bake all --set *.tags=myrepo/plane-{{.target}}:1.2.3 --push
|
||||
#
|
||||
# # Or set variables:
|
||||
# docker buildx bake all --set TAG=1.2.3 --set IMAGE_PREFIX=myrepo/ --push
|
||||
#
|
||||
# Notes:
|
||||
# - The "AIO" image composes from the individual images built here.
|
||||
# - "live" currently uses its app-specific Dockerfile (non-Next.js runtime).
|
||||
|
||||
group "default" {
|
||||
targets = ["web", "space", "admin", "live", "api", "proxy"]
|
||||
}
|
||||
|
||||
group "all" {
|
||||
targets = ["web", "space", "admin", "live", "api", "proxy", "aio"]
|
||||
}
|
||||
|
||||
group "frontend" {
|
||||
targets = ["web", "space", "admin"]
|
||||
}
|
||||
|
||||
variable "TAG" {
|
||||
# Global tag to apply to images
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "IMAGE_PREFIX" {
|
||||
# Optional prefix/registry for image tags, e.g. "ghcr.io/makeplane/" or "yourrepo/"
|
||||
# Leave empty to tag locally (e.g. "plane-web:latest")
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "PLATFORMS" {
|
||||
# List of platforms (e.g., ["linux/amd64", "linux/arm64"])
|
||||
default = ["linux/amd64"]
|
||||
}
|
||||
|
||||
# Common cache configuration for faster CI builds
|
||||
target "with-cache" {
|
||||
cache-from = ["type=gha"]
|
||||
cache-to = ["type=gha,mode=max"]
|
||||
}
|
||||
|
||||
# Common base for Next.js apps using the unified Dockerfile
|
||||
target "common-node" {
|
||||
inherits = ["with-cache"]
|
||||
dockerfile = "Dockerfile.node"
|
||||
context = "."
|
||||
platforms = "${PLATFORMS}"
|
||||
}
|
||||
|
||||
# Frontend apps (Next.js standalone runtime)
|
||||
target "web" {
|
||||
inherits = ["common-node"]
|
||||
target = "runtime"
|
||||
args = { APP_SCOPE = "web" }
|
||||
tags = ["${IMAGE_PREFIX}plane-web:${TAG}"]
|
||||
}
|
||||
|
||||
target "space" {
|
||||
inherits = ["common-node"]
|
||||
target = "runtime"
|
||||
args = { APP_SCOPE = "space" }
|
||||
tags = ["${IMAGE_PREFIX}plane-space:${TAG}"]
|
||||
}
|
||||
|
||||
target "admin" {
|
||||
inherits = ["common-node"]
|
||||
target = "runtime"
|
||||
args = { APP_SCOPE = "admin" }
|
||||
tags = ["${IMAGE_PREFIX}plane-admin:${TAG}"]
|
||||
}
|
||||
|
||||
# Live app (Node service; not Next.js standalone)
|
||||
# Keeps its dedicated Dockerfile to match current build/run layout
|
||||
target "live" {
|
||||
inherits = ["with-cache"]
|
||||
dockerfile = "apps/live/Dockerfile.live"
|
||||
context = "."
|
||||
platforms = "${PLATFORMS}"
|
||||
tags = ["${IMAGE_PREFIX}plane-live:${TAG}"]
|
||||
}
|
||||
|
||||
# Python API (unified Dockerfile)
|
||||
target "api" {
|
||||
inherits = ["with-cache"]
|
||||
dockerfile = "Dockerfile.api"
|
||||
context = "."
|
||||
target = "runtime"
|
||||
platforms = "${PLATFORMS}"
|
||||
tags = ["${IMAGE_PREFIX}plane-api:${TAG}"]
|
||||
}
|
||||
|
||||
# Proxy (Caddy with plugins)
|
||||
target "proxy" {
|
||||
inherits = ["with-cache"]
|
||||
dockerfile = "Dockerfile.ce"
|
||||
context = "apps/proxy"
|
||||
platforms = "${PLATFORMS}"
|
||||
tags = ["${IMAGE_PREFIX}plane-proxy:${TAG}"]
|
||||
}
|
||||
|
||||
# All-in-one assembler image
|
||||
# Composes from previously built runtime images; override args if you use different tags.
|
||||
target "aio" {
|
||||
inherits = ["with-cache"]
|
||||
dockerfile = "Dockerfile.aio"
|
||||
context = "."
|
||||
platforms = "${PLATFORMS}"
|
||||
contexts = {
|
||||
web_ctx = "target:web"
|
||||
space_ctx = "target:space"
|
||||
admin_ctx = "target:admin"
|
||||
live_ctx = "target:live"
|
||||
api_ctx = "target:api"
|
||||
proxy_ctx = "target:proxy"
|
||||
}
|
||||
tags = ["${IMAGE_PREFIX}plane-aio:${TAG}"]
|
||||
}
|
||||
+150
-200
@@ -1,9 +1,134 @@
|
||||
# Local development stack with hot reload for web/space/admin/live and Django API.
|
||||
# Uses unified dev targets in plane/Dockerfile.node and plane/Dockerfile.api.
|
||||
# Infra services (Postgres, Redis, RabbitMQ, MinIO) are included.
|
||||
|
||||
services:
|
||||
# Django API in dev mode (bind mounts for instant reload)
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.api
|
||||
target: dev
|
||||
restart: unless-stopped
|
||||
working_dir: /code
|
||||
command: ./bin/docker-entrypoint-api-local.sh
|
||||
env_file:
|
||||
- ./apps/api/.env
|
||||
environment:
|
||||
USE_MINIO: "1"
|
||||
AWS_S3_ENDPOINT_URL: http://plane-minio:9000
|
||||
MINIO_ENDPOINT_URL: http://plane-minio:9000
|
||||
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-minioadmin}
|
||||
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-minioadmin}
|
||||
AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||
AWS_REGION: ${AWS_REGION:-us-east-1}
|
||||
WEB_URL: ${WEB_URL:-http://localhost:3001}
|
||||
DJANGO_DEBUG_TOOLBAR: ${DJANGO_DEBUG_TOOLBAR:-0}
|
||||
volumes:
|
||||
- ./apps/api:/code
|
||||
depends_on:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
- plane-mq
|
||||
- plane-minio
|
||||
ports:
|
||||
- "8000:8000"
|
||||
|
||||
worker:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.api
|
||||
target: dev
|
||||
restart: unless-stopped
|
||||
working_dir: /code
|
||||
command: ./bin/docker-entrypoint-worker.sh
|
||||
env_file:
|
||||
- ./apps/api/.env
|
||||
environment:
|
||||
USE_MINIO: "1"
|
||||
AWS_S3_ENDPOINT_URL: http://plane-minio:9000
|
||||
MINIO_ENDPOINT_URL: http://plane-minio:9000
|
||||
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-minioadmin}
|
||||
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-minioadmin}
|
||||
AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||
AWS_REGION: ${AWS_REGION:-us-east-1}
|
||||
volumes:
|
||||
- ./apps/api:/code
|
||||
depends_on:
|
||||
- api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
- plane-mq
|
||||
|
||||
beat-worker:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.api
|
||||
target: dev
|
||||
restart: unless-stopped
|
||||
working_dir: /code
|
||||
command: ./bin/docker-entrypoint-beat.sh
|
||||
env_file:
|
||||
- ./apps/api/.env
|
||||
environment:
|
||||
USE_MINIO: "1"
|
||||
AWS_S3_ENDPOINT_URL: http://plane-minio:9000
|
||||
MINIO_ENDPOINT_URL: http://plane-minio:9000
|
||||
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-minioadmin}
|
||||
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-minioadmin}
|
||||
AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||
AWS_REGION: ${AWS_REGION:-us-east-1}
|
||||
volumes:
|
||||
- ./apps/api:/code
|
||||
depends_on:
|
||||
- api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
- plane-mq
|
||||
|
||||
migrator:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.api
|
||||
target: dev
|
||||
restart: "no"
|
||||
working_dir: /code
|
||||
command: ./bin/docker-entrypoint-migrator.sh --settings=plane.settings.local
|
||||
env_file:
|
||||
- ./apps/api/.env
|
||||
environment:
|
||||
USE_MINIO: "1"
|
||||
AWS_S3_ENDPOINT_URL: http://plane-minio:9000
|
||||
MINIO_ENDPOINT_URL: http://plane-minio:9000
|
||||
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-minioadmin}
|
||||
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-minioadmin}
|
||||
AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||
AWS_REGION: ${AWS_REGION:-us-east-1}
|
||||
volumes:
|
||||
- ./apps/api:/code
|
||||
depends_on:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
- plane-minio
|
||||
|
||||
# Infra
|
||||
plane-db:
|
||||
image: postgres:15.7-alpine
|
||||
restart: unless-stopped
|
||||
command: "postgres -c 'max_connections=1000'"
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-plane}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-plane}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-plane}
|
||||
PGDATA: /var/lib/postgresql/data
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
plane-redis:
|
||||
image: valkey/valkey:7.2.5-alpine
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dev_env
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
ports:
|
||||
@@ -12,218 +137,43 @@ services:
|
||||
plane-mq:
|
||||
image: rabbitmq:3.13.6-management-alpine
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dev_env
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-plane}
|
||||
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:-plane}
|
||||
RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_VHOST:-plane}
|
||||
volumes:
|
||||
- rabbitmq_data:/var/lib/rabbitmq
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER}
|
||||
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
|
||||
RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_VHOST}
|
||||
ports:
|
||||
- "15672:15672"
|
||||
|
||||
plane-minio:
|
||||
image: minio/minio
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dev_env
|
||||
# Bootstrap bucket on first run; suitable for local smoke/dev only
|
||||
entrypoint: >
|
||||
/bin/sh -c "
|
||||
mkdir -p /export/${AWS_S3_BUCKET_NAME} &&
|
||||
minio server /export --console-address ':9090' &
|
||||
sleep 5 &&
|
||||
mc alias set myminio http://localhost:9000 ${AWS_ACCESS_KEY_ID} ${AWS_SECRET_ACCESS_KEY} &&
|
||||
mc mb myminio/${AWS_S3_BUCKET_NAME} -p || true
|
||||
&& tail -f /dev/null
|
||||
set -e
|
||||
mkdir -p /export/${AWS_S3_BUCKET_NAME:-uploads};
|
||||
minio server /export --console-address ':9090' & pid=$!;
|
||||
sleep 5;
|
||||
mc alias set myminio http://plane-minio:9000 ${AWS_ACCESS_KEY_ID:-minioadmin} ${AWS_SECRET_ACCESS_KEY:-minioadmin};
|
||||
mc mb myminio/${AWS_S3_BUCKET_NAME:-uploads} -p || true;
|
||||
wait $pid
|
||||
"
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID:-minioadmin}
|
||||
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY:-minioadmin}
|
||||
volumes:
|
||||
- uploads:/export
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID}
|
||||
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9090:9090"
|
||||
|
||||
plane-db:
|
||||
image: postgres:15.7-alpine
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dev_env
|
||||
command: postgres -c 'max_connections=1000'
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
PGDATA: /var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
# web:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: ./web/Dockerfile.dev
|
||||
# restart: unless-stopped
|
||||
# networks:
|
||||
# - dev_env
|
||||
# volumes:
|
||||
# - ./web:/app/web
|
||||
# env_file:
|
||||
# - ./web/.env
|
||||
# depends_on:
|
||||
# - api
|
||||
# - worker
|
||||
|
||||
# space:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: ./space/Dockerfile.dev
|
||||
# restart: unless-stopped
|
||||
# networks:
|
||||
# - dev_env
|
||||
# volumes:
|
||||
# - ./space:/app/space
|
||||
# depends_on:
|
||||
# - api
|
||||
# - worker
|
||||
# - web
|
||||
|
||||
# admin:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: ./admin/Dockerfile.dev
|
||||
# restart: unless-stopped
|
||||
# networks:
|
||||
# - dev_env
|
||||
# volumes:
|
||||
# - ./admin:/app/admin
|
||||
# depends_on:
|
||||
# - api
|
||||
# - worker
|
||||
# - web
|
||||
|
||||
# live:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: ./live/Dockerfile.dev
|
||||
# restart: unless-stopped
|
||||
# networks:
|
||||
# - dev_env
|
||||
# volumes:
|
||||
# - ./live:/app/live
|
||||
# depends_on:
|
||||
# - api
|
||||
# - worker
|
||||
# - web
|
||||
|
||||
api:
|
||||
build:
|
||||
context: ./apps/api
|
||||
dockerfile: Dockerfile.dev
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dev_env
|
||||
volumes:
|
||||
- ./apps/api:/code
|
||||
command: ./bin/docker-entrypoint-api-local.sh
|
||||
env_file:
|
||||
- ./apps/api/.env
|
||||
depends_on:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
- plane-mq
|
||||
ports:
|
||||
- "8000:8000"
|
||||
|
||||
worker:
|
||||
build:
|
||||
context: ./apps/api
|
||||
dockerfile: Dockerfile.dev
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dev_env
|
||||
volumes:
|
||||
- ./apps/api:/code
|
||||
command: ./bin/docker-entrypoint-worker.sh
|
||||
env_file:
|
||||
- ./apps/api/.env
|
||||
depends_on:
|
||||
- api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
beat-worker:
|
||||
build:
|
||||
context: ./apps/api
|
||||
dockerfile: Dockerfile.dev
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dev_env
|
||||
volumes:
|
||||
- ./apps/api:/code
|
||||
command: ./bin/docker-entrypoint-beat.sh
|
||||
env_file:
|
||||
- ./apps/api/.env
|
||||
depends_on:
|
||||
- api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
migrator:
|
||||
build:
|
||||
context: ./apps/api
|
||||
dockerfile: Dockerfile.dev
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: "no"
|
||||
networks:
|
||||
- dev_env
|
||||
volumes:
|
||||
- ./apps/api:/code
|
||||
command: ./bin/docker-entrypoint-migrator.sh --settings=plane.settings.local
|
||||
env_file:
|
||||
- ./apps/api/.env
|
||||
depends_on:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
# proxy:
|
||||
# build:
|
||||
# context: ./apps/proxy
|
||||
# dockerfile: Dockerfile.ce
|
||||
# restart: unless-stopped
|
||||
# networks:
|
||||
# - dev_env
|
||||
# ports:
|
||||
# - ${LISTEN_HTTP_PORT}:80
|
||||
# - ${LISTEN_HTTPS_PORT}:443
|
||||
# env_file:
|
||||
# - .env
|
||||
# environment:
|
||||
# FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
||||
# BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||
# depends_on:
|
||||
# - api
|
||||
# - web
|
||||
# - space
|
||||
# - admin
|
||||
|
||||
volumes:
|
||||
redisdata:
|
||||
uploads:
|
||||
pgdata:
|
||||
rabbitmq_data:
|
||||
# Node/pnpm caches for speedy dev
|
||||
|
||||
networks:
|
||||
dev_env:
|
||||
driver: bridge
|
||||
# Infra data
|
||||
pgdata:
|
||||
redisdata:
|
||||
rabbitmq_data:
|
||||
uploads:
|
||||
|
||||
+14
-11
@@ -3,9 +3,10 @@ services:
|
||||
container_name: web
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./apps/web/Dockerfile.web
|
||||
dockerfile: ./Dockerfile.node
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
APP_SCOPE: web
|
||||
restart: always
|
||||
depends_on:
|
||||
- api
|
||||
@@ -14,9 +15,10 @@ services:
|
||||
container_name: admin
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./apps/admin/Dockerfile.admin
|
||||
dockerfile: ./Dockerfile.node
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
APP_SCOPE: admin
|
||||
restart: always
|
||||
depends_on:
|
||||
- api
|
||||
@@ -26,9 +28,10 @@ services:
|
||||
container_name: space
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./apps/space/Dockerfile.space
|
||||
dockerfile: ./Dockerfile.node
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
APP_SCOPE: space
|
||||
restart: always
|
||||
depends_on:
|
||||
- api
|
||||
@@ -37,8 +40,8 @@ services:
|
||||
api:
|
||||
container_name: api
|
||||
build:
|
||||
context: ./apps/api
|
||||
dockerfile: Dockerfile.api
|
||||
context: .
|
||||
dockerfile: ./Dockerfile.api
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: always
|
||||
@@ -52,8 +55,8 @@ services:
|
||||
worker:
|
||||
container_name: bgworker
|
||||
build:
|
||||
context: ./apps/api
|
||||
dockerfile: Dockerfile.api
|
||||
context: .
|
||||
dockerfile: ./Dockerfile.api
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: always
|
||||
@@ -68,8 +71,8 @@ services:
|
||||
beat-worker:
|
||||
container_name: beatworker
|
||||
build:
|
||||
context: ./apps/api
|
||||
dockerfile: Dockerfile.api
|
||||
context: .
|
||||
dockerfile: ./Dockerfile.api
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: always
|
||||
@@ -84,8 +87,8 @@ services:
|
||||
migrator:
|
||||
container_name: plane-migrator
|
||||
build:
|
||||
context: ./apps/api
|
||||
dockerfile: Dockerfile.api
|
||||
context: .
|
||||
dockerfile: ./Dockerfile.api
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: no
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Repo-wide tool versions managed by mise
|
||||
# See: https://mise.jdx.dev
|
||||
|
||||
[tools]
|
||||
# Node LTS "Jod" to match .nvmrc and package.json engines
|
||||
node = "22.18.0"
|
||||
|
||||
# Python version to match Dockerfile.api
|
||||
python = "3.12.10"
|
||||
|
||||
# Global CLI utilities installed via pipx
|
||||
# This provides the `ruff` command used by CI and local checks
|
||||
pipx = "latest"
|
||||
"pipx:ruff" = "0.9.7"
|
||||
@@ -1,4 +0,0 @@
|
||||
node_modules
|
||||
build/*
|
||||
dist/*
|
||||
out/*
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
@@ -1,4 +1,6 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
node_modules
|
||||
build/*
|
||||
dist/*
|
||||
out/*
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@ const project = resolve(process.cwd(), "tsconfig.json");
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
extends: ["prettier", "plugin:@typescript-eslint/recommended"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["react", "react-hooks", "@typescript-eslint", "import"],
|
||||
globals: {
|
||||
React: true,
|
||||
@@ -44,10 +43,10 @@ module.exports = {
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
caughtErrorsIgnorePattern: "^_",
|
||||
},
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_",
|
||||
"caughtErrorsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-useless-empty-export": "error",
|
||||
|
||||
@@ -3,8 +3,6 @@ const project = resolve(process.cwd(), "tsconfig.json");
|
||||
|
||||
module.exports = {
|
||||
extends: ["next", "prettier", "plugin:@typescript-eslint/recommended"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["react", "@typescript-eslint", "import"],
|
||||
globals: {
|
||||
React: "readonly",
|
||||
JSX: "readonly",
|
||||
@@ -13,6 +11,7 @@ module.exports = {
|
||||
node: true,
|
||||
browser: true,
|
||||
},
|
||||
plugins: ["react", "@typescript-eslint", "import"],
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
typescript: {
|
||||
@@ -43,10 +42,10 @@ module.exports = {
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
caughtErrorsIgnorePattern: "^_",
|
||||
},
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_",
|
||||
"caughtErrorsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-useless-empty-export": "error",
|
||||
|
||||
@@ -3,7 +3,6 @@ const project = resolve(process.cwd(), "tsconfig.json");
|
||||
|
||||
module.exports = {
|
||||
extends: ["prettier", "plugin:@typescript-eslint/recommended"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
env: {
|
||||
node: true,
|
||||
es6: true,
|
||||
@@ -26,33 +25,10 @@ module.exports = {
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
caughtErrorsIgnorePattern: "^_",
|
||||
},
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_",
|
||||
"caughtErrorsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"import/order": [
|
||||
"warn",
|
||||
{
|
||||
groups: ["builtin", "external", "internal", "parent", "sibling"],
|
||||
pathGroups: [
|
||||
{
|
||||
pattern: "@plane/**",
|
||||
group: "external",
|
||||
position: "after",
|
||||
},
|
||||
{
|
||||
pattern: "@/**",
|
||||
group: "internal",
|
||||
position: "before",
|
||||
},
|
||||
],
|
||||
pathGroupsExcludedImportTypes: ["builtin", "internal", "react"],
|
||||
alphabetize: {
|
||||
order: "asc",
|
||||
caseInsensitive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ export const getValueFromLocalStorage = (key: string, defaultValue: any) => {
|
||||
try {
|
||||
const item = window.localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : defaultValue;
|
||||
} catch (_error) {
|
||||
} catch (error) {
|
||||
window.localStorage.removeItem(key);
|
||||
return defaultValue;
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export const setValueIntoLocalStorage = (key: string, value: any) => {
|
||||
try {
|
||||
window.localStorage.setItem(key, JSON.stringify(value));
|
||||
return true;
|
||||
} catch (_error) {
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js", "plugin:storybook/recommended"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
rules: {
|
||||
"import/order": [
|
||||
"warn",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
build/*
|
||||
dist/*
|
||||
out/*
|
||||
@@ -1,4 +1,6 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
|
||||
@@ -60,7 +60,7 @@ export const AuthForm: React.FC<AuthFormProps> = ({
|
||||
});
|
||||
|
||||
const [passwordStrength, setPasswordStrength] = useState<E_PASSWORD_STRENGTH>(E_PASSWORD_STRENGTH.EMPTY);
|
||||
const [_passwordsMatch, setPasswordsMatch] = useState(false);
|
||||
const [passwordsMatch, setPasswordsMatch] = useState(false);
|
||||
|
||||
const handleInputChange = (field: keyof AuthFormData) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData((prev) => ({
|
||||
|
||||
@@ -8,7 +8,6 @@ import { cn } from "./utils";
|
||||
|
||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const Calendar = ({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const thirtyYearsAgoFirstDay = new Date(currentYear - 30, 0, 1);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
|
||||
Generated
+10
-10
@@ -846,7 +846,7 @@ importers:
|
||||
version: 2.31.0(@typescript-eslint/parser@8.40.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
||||
eslint-plugin-react:
|
||||
specifier: ^7.33.2
|
||||
version: 7.37.3(eslint@8.57.1)
|
||||
version: 7.37.5(eslint@8.57.1)
|
||||
eslint-plugin-react-hooks:
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.0(eslint@8.57.1)
|
||||
@@ -1071,7 +1071,7 @@ importers:
|
||||
version: 0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.13.3(@swc/helpers@0.5.17))(@types/node@22.17.2)(typescript@5.8.3)))
|
||||
autoprefixer:
|
||||
specifier: ^10.4.14
|
||||
version: 10.4.20(postcss@8.5.6)
|
||||
version: 10.4.21(postcss@8.5.6)
|
||||
postcss:
|
||||
specifier: ^8.4.38
|
||||
version: 8.5.6
|
||||
@@ -1243,7 +1243,7 @@ importers:
|
||||
version: 18.3.1
|
||||
autoprefixer:
|
||||
specifier: ^10.4.19
|
||||
version: 10.4.20(postcss@8.5.6)
|
||||
version: 10.4.21(postcss@8.5.6)
|
||||
postcss-cli:
|
||||
specifier: ^11.0.0
|
||||
version: 11.0.1(jiti@1.21.7)(postcss@8.5.6)
|
||||
@@ -3865,8 +3865,8 @@ packages:
|
||||
peerDependencies:
|
||||
postcss: ^8.1.0
|
||||
|
||||
autoprefixer@10.4.20:
|
||||
resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==}
|
||||
autoprefixer@10.4.21:
|
||||
resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -4738,8 +4738,8 @@ packages:
|
||||
peerDependencies:
|
||||
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
|
||||
|
||||
eslint-plugin-react@7.37.3:
|
||||
resolution: {integrity: sha512-DomWuTQPFYZwF/7c9W2fkKkStqZmBd3uugfqBYLdkZ3Hii23WzZuOLUskGxB8qkSKqftxEeGL1TB2kMhrce0jA==}
|
||||
eslint-plugin-react@7.37.5:
|
||||
resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==}
|
||||
engines: {node: '>=4'}
|
||||
peerDependencies:
|
||||
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7
|
||||
@@ -10722,7 +10722,7 @@ snapshots:
|
||||
postcss: 8.5.6
|
||||
postcss-value-parser: 4.2.0
|
||||
|
||||
autoprefixer@10.4.20(postcss@8.5.6):
|
||||
autoprefixer@10.4.21(postcss@8.5.6):
|
||||
dependencies:
|
||||
browserslist: 4.25.2
|
||||
caniuse-lite: 1.0.30001735
|
||||
@@ -11607,7 +11607,7 @@ snapshots:
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.40.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
|
||||
eslint-plugin-react: 7.37.3(eslint@8.57.1)
|
||||
eslint-plugin-react: 7.37.5(eslint@8.57.1)
|
||||
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1)
|
||||
optionalDependencies:
|
||||
typescript: 5.8.3
|
||||
@@ -11715,7 +11715,7 @@ snapshots:
|
||||
dependencies:
|
||||
eslint: 8.57.1
|
||||
|
||||
eslint-plugin-react@7.37.3(eslint@8.57.1):
|
||||
eslint-plugin-react@7.37.5(eslint@8.57.1):
|
||||
dependencies:
|
||||
array-includes: 3.1.9
|
||||
array.prototype.findlast: 1.2.5
|
||||
|
||||
Executable
+320
@@ -0,0 +1,320 @@
|
||||
#!/usr/bin/env bash
|
||||
# plane/scripts/smoke-aio.sh
|
||||
# Smoke test for the all-in-one (AIO) image. It:
|
||||
# - Runs the plane-aio image with minimal required environment
|
||||
# - Waits for Caddy to come up
|
||||
# - Probes proxied endpoints:
|
||||
# / (web)
|
||||
# /spaces (space)
|
||||
# /god-mode (admin)
|
||||
# /live/health (live)
|
||||
# - Prints container logs on failure and exits non-zero
|
||||
#
|
||||
# Usage examples:
|
||||
# ./scripts/smoke-aio.sh
|
||||
# ./scripts/smoke-aio.sh --image yourrepo/plane-aio:latest --port 18080
|
||||
# ./scripts/smoke-aio.sh --attempts 90 --sleep 1 --keep
|
||||
# ./scripts/smoke-aio.sh --docker-flags "--network host"
|
||||
#
|
||||
# Dependencies: bash, docker, curl
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# -------------------------------
|
||||
# Defaults
|
||||
# -------------------------------
|
||||
IMAGE_DEFAULT="plane-aio:latest"
|
||||
HOST_DEFAULT="127.0.0.1"
|
||||
PORT_DEFAULT=8080
|
||||
ATTEMPTS_DEFAULT=60
|
||||
SLEEP_DEFAULT=2
|
||||
KEEP_DEFAULT=0
|
||||
PULL_DEFAULT=0
|
||||
DOCKER_FLAGS_DEFAULT=""
|
||||
|
||||
# Required env for AIO (values are placeholders for smoke only)
|
||||
DOMAIN_NAME_DEFAULT="localhost"
|
||||
DATABASE_URL_DEFAULT="postgresql://plane:plane@127.0.0.1:15432/plane"
|
||||
REDIS_URL_DEFAULT="redis://127.0.0.1:16379"
|
||||
AMQP_URL_DEFAULT="amqp://plane:plane@127.0.0.1:15673/plane"
|
||||
AWS_REGION_DEFAULT="us-east-1"
|
||||
AWS_ACCESS_KEY_ID_DEFAULT="smoke"
|
||||
AWS_SECRET_ACCESS_KEY_DEFAULT="smoke"
|
||||
AWS_S3_BUCKET_NAME_DEFAULT="smoke-bucket"
|
||||
AWS_S3_ENDPOINT_URL_DEFAULT="http://127.0.0.1:19000"
|
||||
SITE_ADDRESS_DEFAULT=":80"
|
||||
FILE_SIZE_LIMIT_DEFAULT="5242880"
|
||||
|
||||
# -------------------------------
|
||||
# State
|
||||
# -------------------------------
|
||||
IMAGE="$IMAGE_DEFAULT"
|
||||
HOST="$HOST_DEFAULT"
|
||||
PORT="$PORT_DEFAULT"
|
||||
ATTEMPTS="$ATTEMPTS_DEFAULT"
|
||||
SLEEP_SECS="$SLEEP_DEFAULT"
|
||||
KEEP="$KEEP_DEFAULT"
|
||||
PULL="$PULL_DEFAULT"
|
||||
DOCKER_FLAGS="$DOCKER_FLAGS_DEFAULT"
|
||||
|
||||
# AIO env values (overridable via flags)
|
||||
DOMAIN_NAME="$DOMAIN_NAME_DEFAULT"
|
||||
DATABASE_URL="$DATABASE_URL_DEFAULT"
|
||||
REDIS_URL="$REDIS_URL_DEFAULT"
|
||||
AMQP_URL="$AMQP_URL_DEFAULT"
|
||||
AWS_REGION="$AWS_REGION_DEFAULT"
|
||||
AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID_DEFAULT"
|
||||
AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY_DEFAULT"
|
||||
AWS_S3_BUCKET_NAME="$AWS_S3_BUCKET_NAME_DEFAULT"
|
||||
AWS_S3_ENDPOINT_URL="$AWS_S3_ENDPOINT_URL_DEFAULT"
|
||||
SITE_ADDRESS="$SITE_ADDRESS_DEFAULT"
|
||||
FILE_SIZE_LIMIT="$FILE_SIZE_LIMIT_DEFAULT"
|
||||
|
||||
TMP_DIR="$(mktemp -d -t plane-aio-smoke.XXXXXX)"
|
||||
CONTAINER="plane-aio-smoke-$$"
|
||||
|
||||
# Endpoints to probe via Caddy
|
||||
# Endpoint selection flags (1 = test, 0 = skip)
|
||||
RUN_WEB=1
|
||||
RUN_SPACE=1
|
||||
RUN_ADMIN=1
|
||||
RUN_LIVE=1
|
||||
|
||||
# Build PATHS dynamically later based on RUN_* flags
|
||||
declare -a PATHS=()
|
||||
declare -A NAMES=( ["/"]="web" ["/spaces"]="space" ["/god-mode"]="admin" ["/live/health"]="live" )
|
||||
FAILURES=0
|
||||
|
||||
# -------------------------------
|
||||
# Helpers
|
||||
# -------------------------------
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [options]
|
||||
|
||||
Options:
|
||||
--image <name:tag> AIO image to run (default: ${IMAGE_DEFAULT})
|
||||
--host <ip/host> Hostname/IP to probe (default: ${HOST_DEFAULT})
|
||||
--port <port> Host port to map container 80 to (default: ${PORT_DEFAULT})
|
||||
|
||||
--attempts <n> Attempts before failure (default: ${ATTEMPTS_DEFAULT})
|
||||
--sleep <seconds> Sleep between attempts (default: ${SLEEP_DEFAULT})
|
||||
--keep Do not remove the container after the test
|
||||
--pull docker pull the image before run
|
||||
--docker-flags "<flags>" Extra flags for docker run (e.g., --network)
|
||||
|
||||
# Override required env vars for AIO start script:
|
||||
--domain-name <val> (default: ${DOMAIN_NAME_DEFAULT})
|
||||
--database-url <val> (default: ${DATABASE_URL_DEFAULT})
|
||||
--redis-url <val> (default: ${REDIS_URL_DEFAULT})
|
||||
--amqp-url <val> (default: ${AMQP_URL_DEFAULT})
|
||||
--aws-region <val> (default: ${AWS_REGION_DEFAULT})
|
||||
--aws-access-key-id <val> (default: ${AWS_ACCESS_KEY_ID_DEFAULT})
|
||||
--aws-secret-access-key <val> (default: ${AWS_SECRET_ACCESS_KEY_DEFAULT})
|
||||
--aws-s3-bucket-name <val> (default: ${AWS_S3_BUCKET_NAME_DEFAULT})
|
||||
--aws-s3-endpoint-url <val> (default: ${AWS_S3_ENDPOINT_URL_DEFAULT})
|
||||
--site-address <val> (default: ${SITE_ADDRESS_DEFAULT})
|
||||
--file-size-limit <bytes> (default: ${FILE_SIZE_LIMIT_DEFAULT})
|
||||
|
||||
-h, --help Show help and exit
|
||||
|
||||
Examples:
|
||||
$(basename "$0")
|
||||
$(basename "$0") --image yourrepo/plane-aio:latest --port 18080 --attempts 90 --sleep 1
|
||||
EOF
|
||||
}
|
||||
|
||||
exists_cmd() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
log() { printf "%s\n" "$*"; }
|
||||
log_ok() { printf "\033[32m%s\033[0m\n" "$*"; }
|
||||
log_warn() { printf "\033[33m%s\033[0m\n" "$*"; }
|
||||
log_err() { printf "\033[31m%s\033[0m\n" "$*"; }
|
||||
|
||||
cleanup() {
|
||||
local code=$?
|
||||
if [[ $KEEP -eq 0 ]]; then
|
||||
docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
|
||||
else
|
||||
log "Keeping container: $CONTAINER"
|
||||
fi
|
||||
rm -rf "$TMP_DIR" >/dev/null 2>&1 || true
|
||||
exit $code
|
||||
}
|
||||
|
||||
is_port_in_use() {
|
||||
local host="$1" port="$2"
|
||||
if exists_cmd ss; then
|
||||
ss -ltn 2>/dev/null | awk '{print $4}' | grep -q ":${port}\\>" && return 0
|
||||
elif exists_cmd lsof; then
|
||||
lsof -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1 && return 0
|
||||
elif exists_cmd netstat; then
|
||||
netstat -ltn 2>/dev/null | awk '{print $4}' | grep -q ":${port}\\>" && return 0
|
||||
else
|
||||
local code
|
||||
code="$(curl -sS -m 1 -o /dev/null -w "%{http_code}" "http://${host}:${port}/" || true)"
|
||||
[[ "$code" != "000" ]] && return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# -------------------------------
|
||||
# Arg parsing
|
||||
# -------------------------------
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--image) IMAGE="$2"; shift 2;;
|
||||
--host) HOST="$2"; shift 2;;
|
||||
--port) PORT="$2"; shift 2;;
|
||||
|
||||
--attempts) ATTEMPTS="$2"; shift 2;;
|
||||
--sleep) SLEEP_SECS="$2"; shift 2;;
|
||||
--keep) KEEP=1; shift;;
|
||||
--pull) PULL=1; shift;;
|
||||
--docker-flags) DOCKER_FLAGS="$2"; shift 2;;
|
||||
|
||||
--domain-name) DOMAIN_NAME="$2"; shift 2;;
|
||||
--database-url) DATABASE_URL="$2"; shift 2;;
|
||||
--redis-url) REDIS_URL="$2"; shift 2;;
|
||||
--amqp-url) AMQP_URL="$2"; shift 2;;
|
||||
--aws-region) AWS_REGION="$2"; shift 2;;
|
||||
--aws-access-key-id) AWS_ACCESS_KEY_ID="$2"; shift 2;;
|
||||
--aws-secret-access-key) AWS_SECRET_ACCESS_KEY="$2"; shift 2;;
|
||||
--aws-s3-bucket-name) AWS_S3_BUCKET_NAME="$2"; shift 2;;
|
||||
--aws-s3-endpoint-url) AWS_S3_ENDPOINT_URL="$2"; shift 2;;
|
||||
--site-address) SITE_ADDRESS="$2"; shift 2;;
|
||||
--file-size-limit) FILE_SIZE_LIMIT="$2"; shift 2;;
|
||||
--skip-web) RUN_WEB=0; shift;;
|
||||
--skip-space) RUN_SPACE=0; shift;;
|
||||
--skip-admin) RUN_ADMIN=0; shift;;
|
||||
--skip-live) RUN_LIVE=0; shift;;
|
||||
|
||||
-h|--help) usage; exit 0;;
|
||||
*) log_err "Unknown arg: $1"; usage; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
# -------------------------------
|
||||
# Pre-flight
|
||||
# -------------------------------
|
||||
exists_cmd docker || { log_err "docker is required"; exit 1; }
|
||||
exists_cmd curl || { log_err "curl is required"; exit 1; }
|
||||
|
||||
log "AIO smoke starting..."
|
||||
log " Image: ${IMAGE}"
|
||||
log " Host: ${HOST}"
|
||||
log " Port: ${PORT}"
|
||||
log " Attempts: ${ATTEMPTS}"
|
||||
log " Sleep (s): ${SLEEP_SECS}"
|
||||
log " Keep: ${KEEP}"
|
||||
log " Pull: ${PULL}"
|
||||
log " Docker flags: ${DOCKER_FLAGS:-<none>}"
|
||||
# Parse DOCKER_FLAGS string into an array for safe docker invocation
|
||||
if [[ -n "${DOCKER_FLAGS:-}" ]]; then
|
||||
read -r -a DOCKER_FLAGS_ARRAY <<< "$DOCKER_FLAGS"
|
||||
else
|
||||
DOCKER_FLAGS_ARRAY=()
|
||||
fi
|
||||
|
||||
if is_port_in_use "$HOST" "$PORT"; then
|
||||
log_err "Port ${HOST}:${PORT} appears to be in use. Use --port to override."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $PULL -eq 1 ]]; then
|
||||
log "Pulling ${IMAGE} ..."
|
||||
docker pull "$IMAGE" >/dev/null
|
||||
fi
|
||||
|
||||
# -------------------------------
|
||||
# Run container
|
||||
# -------------------------------
|
||||
log "Starting AIO container: ${CONTAINER}"
|
||||
|
||||
if ! docker run -d --name "$CONTAINER" \
|
||||
-p "${PORT}:80" \
|
||||
-e DOMAIN_NAME="$DOMAIN_NAME" \
|
||||
-e DATABASE_URL="$DATABASE_URL" \
|
||||
-e REDIS_URL="$REDIS_URL" \
|
||||
-e AMQP_URL="$AMQP_URL" \
|
||||
-e AWS_REGION="$AWS_REGION" \
|
||||
-e AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID" \
|
||||
-e AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY" \
|
||||
-e AWS_S3_BUCKET_NAME="$AWS_S3_BUCKET_NAME" \
|
||||
-e AWS_S3_ENDPOINT_URL="$AWS_S3_ENDPOINT_URL" \
|
||||
-e SITE_ADDRESS="$SITE_ADDRESS" \
|
||||
-e FILE_SIZE_LIMIT="$FILE_SIZE_LIMIT" \
|
||||
"${DOCKER_FLAGS_ARRAY[@]}" \
|
||||
"$IMAGE" >"$TMP_DIR/run.out" 2>"$TMP_DIR/run.err"
|
||||
then
|
||||
log_err "Failed to start AIO container"
|
||||
cat "$TMP_DIR/run.err" 1>&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# -------------------------------
|
||||
# Select endpoints to probe
|
||||
# -------------------------------
|
||||
PATHS=()
|
||||
[[ $RUN_WEB -eq 1 ]] && PATHS+=("/")
|
||||
[[ $RUN_SPACE -eq 1 ]] && PATHS+=("/spaces")
|
||||
[[ $RUN_ADMIN -eq 1 ]] && PATHS+=("/god-mode")
|
||||
[[ $RUN_LIVE -eq 1 ]] && PATHS+=("/live/health")
|
||||
|
||||
# -------------------------------
|
||||
# Probe endpoints
|
||||
# -------------------------------
|
||||
for path in "${PATHS[@]}"; do
|
||||
name="${NAMES[$path]}"
|
||||
url="http://${HOST}:${PORT}${path}"
|
||||
i=1
|
||||
status=""
|
||||
early_logs_printed=0
|
||||
log "Probing ${name}: ${url}"
|
||||
while [[ $i -le $ATTEMPTS ]]; do
|
||||
errfile="$TMP_DIR/${name}.curl.err"
|
||||
status="$(curl --connect-timeout 5 --max-time 10 -sS -o /dev/null -w "%{http_code}" -L "$url" 2>"$errfile" || true)"
|
||||
if [[ "$status" == "200" ]]; then
|
||||
log_ok "Success: ${name} responded 200 at ${url}"
|
||||
break
|
||||
fi
|
||||
|
||||
# Print early container logs after a few failures
|
||||
if [[ $i -eq 5 && $early_logs_printed -eq 0 ]]; then
|
||||
log "----- Early container logs (${CONTAINER}) -----"
|
||||
docker logs "$CONTAINER" || true
|
||||
log "-----------------------------------------------"
|
||||
early_logs_printed=1
|
||||
fi
|
||||
|
||||
# Detect container exit
|
||||
state="$(docker ps -a --filter "name=${CONTAINER}" --format '{{.Status}}' || true)"
|
||||
if [[ -n "$state" ]] && echo "$state" | grep -qi "^exited"; then
|
||||
log_err "Container exited early: ${state}"
|
||||
log "----- Container logs (${CONTAINER}) -----"
|
||||
docker logs "$CONTAINER" || true
|
||||
log "-----------------------------------------"
|
||||
FAILURES=$((FAILURES+1))
|
||||
break
|
||||
fi
|
||||
|
||||
log "Waiting ${name} (attempt ${i}/${ATTEMPTS}) -> HTTP ${status}, retrying in ${SLEEP_SECS}s"
|
||||
sleep "$SLEEP_SECS"
|
||||
i=$((i+1))
|
||||
done
|
||||
|
||||
if [[ "$status" != "200" ]]; then
|
||||
log_err "Failure: ${name} did not return 200 at ${url}"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $FAILURES -gt 0 ]]; then
|
||||
log_err "AIO smoke finished with ${FAILURES} failure(s)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_ok "AIO smoke finished successfully"
|
||||
exit 0
|
||||
Executable
+404
@@ -0,0 +1,404 @@
|
||||
#!/usr/bin/env bash
|
||||
# plane/scripts/smoke.sh
|
||||
# Portable smoke test for Plane frontend images (web, space, admin, live).
|
||||
# - Runs each image in a container bound to a host port
|
||||
# - Probes an HTTP path until it returns 200 OK (or times out)
|
||||
# - Prints container logs on failure and exits non-zero
|
||||
#
|
||||
# Examples:
|
||||
# ./scripts/smoke.sh
|
||||
# ./scripts/smoke.sh --web plane-web:latest --space plane-space:latest
|
||||
# ./scripts/smoke.sh --image-prefix local/ --attempts 90 --sleep 1
|
||||
# ./scripts/smoke.sh --parallel --keep
|
||||
#
|
||||
# Dependencies: bash, docker, curl
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# -------------------------------
|
||||
# Defaults
|
||||
# -------------------------------
|
||||
WEB_IMAGE_DEFAULT="plane-web:latest"
|
||||
SPACE_IMAGE_DEFAULT="plane-space:latest"
|
||||
ADMIN_IMAGE_DEFAULT="plane-admin:latest"
|
||||
LIVE_IMAGE_DEFAULT="plane-live:latest"
|
||||
|
||||
WEB_PORT_DEFAULT=3001
|
||||
SPACE_PORT_DEFAULT=3002
|
||||
ADMIN_PORT_DEFAULT=3003
|
||||
LIVE_PORT_DEFAULT=3005
|
||||
|
||||
WEB_PATH_DEFAULT="/"
|
||||
SPACE_PATH_DEFAULT="/spaces"
|
||||
ADMIN_PATH_DEFAULT="/god-mode"
|
||||
LIVE_PATH_DEFAULT="/live/health"
|
||||
|
||||
ATTEMPTS_DEFAULT=60
|
||||
SLEEP_DEFAULT=2
|
||||
KEEP_DEFAULT=0 # 1 = keep containers after run
|
||||
PULL_DEFAULT=0 # 1 = docker pull before run
|
||||
PARALLEL_DEFAULT=0 # 1 = run tests in parallel
|
||||
HOST_DEFAULT="127.0.0.1"
|
||||
DOCKER_FLAGS_DEFAULT=""
|
||||
|
||||
# Live specific env (can be overridden via --live-env)
|
||||
LIVE_ENV_DEFAULT="-e NODE_ENV=production -e LIVE_BASE_PATH=/live"
|
||||
|
||||
# -------------------------------
|
||||
# State
|
||||
# -------------------------------
|
||||
WEB_IMAGE="$WEB_IMAGE_DEFAULT"
|
||||
SPACE_IMAGE="$SPACE_IMAGE_DEFAULT"
|
||||
ADMIN_IMAGE="$ADMIN_IMAGE_DEFAULT"
|
||||
LIVE_IMAGE="$LIVE_IMAGE_DEFAULT"
|
||||
|
||||
WEB_PORT="$WEB_PORT_DEFAULT"
|
||||
SPACE_PORT="$SPACE_PORT_DEFAULT"
|
||||
ADMIN_PORT="$ADMIN_PORT_DEFAULT"
|
||||
LIVE_PORT="$LIVE_PORT_DEFAULT"
|
||||
|
||||
WEB_PATH="$WEB_PATH_DEFAULT"
|
||||
SPACE_PATH="$SPACE_PATH_DEFAULT"
|
||||
ADMIN_PATH="$ADMIN_PATH_DEFAULT"
|
||||
LIVE_PATH="$LIVE_PATH_DEFAULT"
|
||||
|
||||
ATTEMPTS="$ATTEMPTS_DEFAULT"
|
||||
SLEEP_SECS="$SLEEP_DEFAULT"
|
||||
KEEP="$KEEP_DEFAULT"
|
||||
PULL="$PULL_DEFAULT"
|
||||
PARALLEL="$PARALLEL_DEFAULT"
|
||||
HOST="$HOST_DEFAULT"
|
||||
DOCKER_FLAGS="$DOCKER_FLAGS_DEFAULT"
|
||||
LIVE_ENV="$LIVE_ENV_DEFAULT"
|
||||
|
||||
RUN_WEB=1
|
||||
RUN_SPACE=1
|
||||
RUN_ADMIN=1
|
||||
RUN_LIVE=1
|
||||
|
||||
TMP_DIR="$(mktemp -d -t plane-smoke.XXXXXX)"
|
||||
CONTAINERS_FILE="$TMP_DIR/containers.txt"
|
||||
: > "$CONTAINERS_FILE"
|
||||
CONTAINERS=()
|
||||
PIDS=()
|
||||
FAILURES=0
|
||||
|
||||
# -------------------------------
|
||||
# Helpers
|
||||
# -------------------------------
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [options]
|
||||
|
||||
Options:
|
||||
--web <image[:tag]> Image for web (default: ${WEB_IMAGE_DEFAULT})
|
||||
--space <image[:tag]> Image for space (default: ${SPACE_IMAGE_DEFAULT})
|
||||
--admin <image[:tag]> Image for admin (default: ${ADMIN_IMAGE_DEFAULT})
|
||||
--live <image[:tag]> Image for live (default: ${LIVE_IMAGE_DEFAULT})
|
||||
|
||||
--skip-web Skip web
|
||||
--skip-space Skip space
|
||||
--skip-admin Skip admin
|
||||
--skip-live Skip live
|
||||
|
||||
--web-port <port> Host port for web (default: ${WEB_PORT_DEFAULT})
|
||||
--space-port <port> Host port for space (default: ${SPACE_PORT_DEFAULT})
|
||||
--admin-port <port> Host port for admin (default: ${ADMIN_PORT_DEFAULT})
|
||||
--live-port <port> Host port for live (default: ${LIVE_PORT_DEFAULT})
|
||||
|
||||
--web-path <path> Path to probe for web (default: ${WEB_PATH_DEFAULT})
|
||||
--space-path <path> Path to probe for space (default: ${SPACE_PATH_DEFAULT})
|
||||
--admin-path <path> Path to probe for admin (default: ${ADMIN_PATH_DEFAULT})
|
||||
--live-path <path> Path to probe for live (default: ${LIVE_PATH_DEFAULT})
|
||||
|
||||
--host <ip/host> Hostname/IP to probe (default: ${HOST_DEFAULT})
|
||||
--attempts <n> Attempts before failure (default: ${ATTEMPTS_DEFAULT})
|
||||
--sleep <seconds> Sleep between attempts (default: ${SLEEP_DEFAULT})
|
||||
--pull docker pull each image before run
|
||||
--keep Do not remove containers after tests
|
||||
--parallel Run tests in parallel
|
||||
--docker-flags "<flags>" Extra flags passed to docker run (e.g. --network)
|
||||
--image-prefix <prefix> Prefix for all images (e.g. "local/"), overrides individual images
|
||||
--live-env "<flags>" Override env flags for 'live' container (default: ${LIVE_ENV_DEFAULT})
|
||||
|
||||
-h, --help Show this help and exit
|
||||
|
||||
Examples:
|
||||
$(basename "$0")
|
||||
$(basename "$0") --web plane-web:latest --space plane-space:latest
|
||||
$(basename "$0") --image-prefix ghcr.io/yourorg/ --attempts 90 --sleep 1
|
||||
EOF
|
||||
}
|
||||
|
||||
exists_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
is_port_in_use() {
|
||||
local host="$1"
|
||||
local port="$2"
|
||||
if exists_cmd ss; then
|
||||
ss -ltn 2>/dev/null | awk '{print $4}' | grep -q ":${port}\\>" && return 0
|
||||
elif exists_cmd lsof; then
|
||||
lsof -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1 && return 0
|
||||
elif exists_cmd netstat; then
|
||||
netstat -ltn 2>/dev/null | awk '{print $4}' | grep -q ":${port}\\>" && return 0
|
||||
else
|
||||
# Fallback: if we can get any HTTP response, assume in use
|
||||
local code
|
||||
code="$(curl -sS -m 1 -o /dev/null -w "%{http_code}" "http://${host}:${port}/" || true)"
|
||||
[[ "$code" != "000" ]] && return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
local code=$?
|
||||
if [[ $KEEP -eq 0 ]]; then
|
||||
if [[ -f "$CONTAINERS_FILE" ]]; then
|
||||
while IFS= read -r c; do
|
||||
[[ -n "$c" ]] || continue
|
||||
docker rm -f "$c" >/dev/null 2>&1 || true
|
||||
done < <(sort -u "$CONTAINERS_FILE")
|
||||
fi
|
||||
fi
|
||||
rm -rf "$TMP_DIR" >/dev/null 2>&1 || true
|
||||
exit $code
|
||||
}
|
||||
|
||||
log() { printf "%s\n" "$*"; }
|
||||
log_ok() { printf "\033[32m%s\033[0m\n" "$*"; }
|
||||
log_warn() { printf "\033[33m%s\033[0m\n" "$*"; }
|
||||
log_err() { printf "\033[31m%s\033[0m\n" "$*"; }
|
||||
|
||||
# name, image, port, path, envflags
|
||||
run_and_probe() {
|
||||
local name="$1"
|
||||
local image="$2"
|
||||
local port="$3"
|
||||
local path="$4"
|
||||
local envflags="$5"
|
||||
local container="${name}-smoke-$$"
|
||||
|
||||
if [[ $PULL -eq 1 ]]; then
|
||||
log "Pulling ${image} ..."
|
||||
docker pull "$image" >/dev/null
|
||||
fi
|
||||
|
||||
# Pre-flight: ensure host port is not already in use
|
||||
if is_port_in_use "$HOST" "$port"; then
|
||||
log_err "Port ${HOST}:${port} appears to be in use; aborting ${name} smoke"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "Starting ${name}: image=${image} port=${port} path=${path}"
|
||||
if ! cid=$(docker run -d --name "$container" -p "${port}:3000" $envflags $DOCKER_FLAGS "$image" 2>"$TMP_DIR/${container}.err"); then
|
||||
# If the image tag is :ci-smoke and it fails, try falling back to :latest
|
||||
base="${image%:*}"
|
||||
tag="${image##*:}"
|
||||
if [[ "$tag" == "$image" ]]; then
|
||||
tag=""
|
||||
fi
|
||||
if [[ "$tag" == "ci-smoke" ]]; then
|
||||
alt_image="${base}:latest"
|
||||
log_warn "Failed to start ${name} with ${image}, retrying with ${alt_image} ..."
|
||||
if ! cid=$(docker run -d --name "$container" -p "${port}:3000" $envflags $DOCKER_FLAGS "$alt_image" 2>"$TMP_DIR/${container}.err"); then
|
||||
log_err "Failed to start container ${container}"
|
||||
cat "$TMP_DIR/${container}.err" 1>&2 || true
|
||||
return 1
|
||||
else
|
||||
image="$alt_image"
|
||||
fi
|
||||
else
|
||||
log_err "Failed to start container ${container}"
|
||||
cat "$TMP_DIR/${container}.err" 1>&2 || true
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
printf "%s\n" "$container" >>"$CONTAINERS_FILE"
|
||||
|
||||
local url="http://${HOST}:${port}${path}"
|
||||
local i=1
|
||||
local status=""
|
||||
local conn_reset_count=0
|
||||
local logs_printed=0
|
||||
while [[ $i -le $ATTEMPTS ]]; do
|
||||
local errfile="$TMP_DIR/${container}.curl.err"
|
||||
status="$(curl -sS -o /dev/null -w "%{http_code}" -L "$url" 2>"$errfile" || true)"
|
||||
if [[ "$status" == "200" ]]; then
|
||||
log_ok "Success: ${name} responded 200 at ${url}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# If repeated connection issues, show early container logs once
|
||||
if grep -qiE "connection reset|failed to connect|connection refused" "$errfile" 2>/dev/null; then
|
||||
conn_reset_count=$((conn_reset_count+1))
|
||||
if [[ $conn_reset_count -ge 3 && $logs_printed -eq 0 ]]; then
|
||||
log "----- ${name} early logs (${container}) -----"
|
||||
docker logs "$container" || true
|
||||
log "---------------------------------------------"
|
||||
logs_printed=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Detect if container exited early
|
||||
local state
|
||||
state="$(docker ps -a --filter "name=${container}" --format '{{.Status}}' || true)"
|
||||
if [[ -n "$state" ]] && echo "$state" | grep -qi "^exited"; then
|
||||
log_err "${name} container exited early: ${state}"
|
||||
log "----- ${name} container logs (${container}) -----"
|
||||
docker logs "$container" || true
|
||||
log "-----------------------------------------------"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "Waiting ${name} (attempt ${i}/${ATTEMPTS}) -> HTTP ${status}, retrying in ${SLEEP_SECS}s"
|
||||
sleep "$SLEEP_SECS"
|
||||
i=$((i+1))
|
||||
done
|
||||
|
||||
log_err "Failure: ${name} did not return 200 at ${url} after ${ATTEMPTS} attempts"
|
||||
log "----- ${name} container logs (${container}) -----"
|
||||
docker logs "$container" || true
|
||||
log "-----------------------------------------------"
|
||||
return 1
|
||||
}
|
||||
|
||||
# -------------------------------
|
||||
# Parse args
|
||||
# -------------------------------
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--web) WEB_IMAGE="$2"; shift 2;;
|
||||
--space) SPACE_IMAGE="$2"; shift 2;;
|
||||
--admin) ADMIN_IMAGE="$2"; shift 2;;
|
||||
--live) LIVE_IMAGE="$2"; shift 2;;
|
||||
|
||||
--skip-web) RUN_WEB=0; shift;;
|
||||
--skip-space) RUN_SPACE=0; shift;;
|
||||
--skip-admin) RUN_ADMIN=0; shift;;
|
||||
--skip-live) RUN_LIVE=0; shift;;
|
||||
|
||||
--web-port) WEB_PORT="$2"; shift 2;;
|
||||
--space-port) SPACE_PORT="$2"; shift 2;;
|
||||
--admin-port) ADMIN_PORT="$2"; shift 2;;
|
||||
--live-port) LIVE_PORT="$2"; shift 2;;
|
||||
|
||||
--web-path) WEB_PATH="$2"; shift 2;;
|
||||
--space-path) SPACE_PATH="$2"; shift 2;;
|
||||
--admin-path) ADMIN_PATH="$2"; shift 2;;
|
||||
--live-path) LIVE_PATH="$2"; shift 2;;
|
||||
|
||||
--host) HOST="$2"; shift 2;;
|
||||
--attempts) ATTEMPTS="$2"; shift 2;;
|
||||
--sleep) SLEEP_SECS="$2"; shift 2;;
|
||||
--pull) PULL=1; shift;;
|
||||
--keep) KEEP=1; shift;;
|
||||
--parallel) PARALLEL=1; shift;;
|
||||
--docker-flags) DOCKER_FLAGS="$2"; shift 2;;
|
||||
--live-env) LIVE_ENV="$2"; shift 2;;
|
||||
|
||||
--image-prefix)
|
||||
local prefix="$2"
|
||||
WEB_IMAGE="${prefix}plane-web:latest"
|
||||
SPACE_IMAGE="${prefix}plane-space:latest"
|
||||
ADMIN_IMAGE="${prefix}plane-admin:latest"
|
||||
LIVE_IMAGE="${prefix}plane-live:latest"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help) usage; exit 0;;
|
||||
*)
|
||||
log_err "Unknown argument: $1"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
# -------------------------------
|
||||
# Pre-flight
|
||||
# -------------------------------
|
||||
exists_cmd docker || { log_err "docker is required"; exit 1; }
|
||||
exists_cmd curl || { log_err "curl is required"; exit 1; }
|
||||
|
||||
log "Smoke test starting..."
|
||||
log " Host: ${HOST}"
|
||||
log " Attempts: ${ATTEMPTS}"
|
||||
log " Sleep (s): ${SLEEP_SECS}"
|
||||
log " Keep: ${KEEP}"
|
||||
log " Pull: ${PULL}"
|
||||
log " Parallel: ${PARALLEL}"
|
||||
log " Docker flags: ${DOCKER_FLAGS:-<none>}"
|
||||
|
||||
# -------------------------------
|
||||
# Run tests
|
||||
# -------------------------------
|
||||
# Define a small runner wrapper that writes a status code to tmp
|
||||
service_job() {
|
||||
local name="$1" image="$2" port="$3" path="$4" envflags="$5" out="$6"
|
||||
if run_and_probe "$name" "$image" "$port" "$path" "$envflags"; then
|
||||
echo "OK" > "$out"
|
||||
else
|
||||
echo "FAIL" > "$out"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ $PARALLEL -eq 1 ]]; then
|
||||
if [[ $RUN_WEB -eq 1 ]]; then
|
||||
service_job "web" "$WEB_IMAGE" "$WEB_PORT" "$WEB_PATH" "" "$TMP_DIR/web.status" &
|
||||
PIDS+=($!)
|
||||
fi
|
||||
if [[ $RUN_SPACE -eq 1 ]]; then
|
||||
service_job "space" "$SPACE_IMAGE" "$SPACE_PORT" "$SPACE_PATH" "" "$TMP_DIR/space.status" &
|
||||
PIDS+=($!)
|
||||
fi
|
||||
if [[ $RUN_ADMIN -eq 1 ]]; then
|
||||
service_job "admin" "$ADMIN_IMAGE" "$ADMIN_PORT" "$ADMIN_PATH" "" "$TMP_DIR/admin.status" &
|
||||
PIDS+=($!)
|
||||
fi
|
||||
if [[ $RUN_LIVE -eq 1 ]]; then
|
||||
service_job "live" "$LIVE_IMAGE" "$LIVE_PORT" "$LIVE_PATH" "$LIVE_ENV" "$TMP_DIR/live.status" &
|
||||
PIDS+=($!)
|
||||
fi
|
||||
|
||||
for pid in "${PIDS[@]}"; do
|
||||
wait "$pid" || true
|
||||
done
|
||||
|
||||
for svc in web space admin live; do
|
||||
[[ $svc == "web" && $RUN_WEB -eq 0 ]] && continue
|
||||
[[ $svc == "space" && $RUN_SPACE -eq 0 ]] && continue
|
||||
[[ $svc == "admin" && $RUN_ADMIN -eq 0 ]] && continue
|
||||
[[ $svc == "live" && $RUN_LIVE -eq 0 ]] && continue
|
||||
status_file="$TMP_DIR/${svc}.status"
|
||||
if [[ -f "$status_file" && "$(cat "$status_file")" == "OK" ]]; then
|
||||
log_ok "${svc}: OK"
|
||||
else
|
||||
log_err "${svc}: FAIL"
|
||||
FAILURES=$((FAILURES+1))
|
||||
fi
|
||||
done
|
||||
else
|
||||
if [[ $RUN_WEB -eq 1 ]]; then
|
||||
if ! run_and_probe "web" "$WEB_IMAGE" "$WEB_PORT" "$WEB_PATH" ""; then FAILURES=$((FAILURES+1)); fi
|
||||
fi
|
||||
if [[ $RUN_SPACE -eq 1 ]]; then
|
||||
if ! run_and_probe "space" "$SPACE_IMAGE" "$SPACE_PORT" "$SPACE_PATH" ""; then FAILURES=$((FAILURES+1)); fi
|
||||
fi
|
||||
if [[ $RUN_ADMIN -eq 1 ]]; then
|
||||
if ! run_and_probe "admin" "$ADMIN_IMAGE" "$ADMIN_PORT" "$ADMIN_PATH" ""; then FAILURES=$((FAILURES+1)); fi
|
||||
fi
|
||||
if [[ $RUN_LIVE -eq 1 ]]; then
|
||||
if ! run_and_probe "live" "$LIVE_IMAGE" "$LIVE_PORT" "$LIVE_PATH" "$LIVE_ENV"; then FAILURES=$((FAILURES+1)); fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $FAILURES -gt 0 ]]; then
|
||||
log_err "Smoke test finished with ${FAILURES} failure(s)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_ok "Smoke test finished successfully"
|
||||
exit 0
|
||||
Reference in New Issue
Block a user