Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f04220818c | |||
| 98562a55c7 |
@@ -8,13 +8,6 @@ PGDATA="/var/lib/postgresql/data"
|
||||
REDIS_HOST="plane-redis"
|
||||
REDIS_PORT="6379"
|
||||
|
||||
# RabbitMQ Settings
|
||||
RABBITMQ_HOST="plane-mq"
|
||||
RABBITMQ_PORT="5672"
|
||||
RABBITMQ_USER="plane"
|
||||
RABBITMQ_PASSWORD="plane"
|
||||
RABBITMQ_VHOST="plane"
|
||||
|
||||
# AWS Settings
|
||||
AWS_REGION=""
|
||||
AWS_ACCESS_KEY_ID="access-key"
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Adds three new lint plugins over the existing configuration:
|
||||
* This is used to lint staged files only.
|
||||
* We should remove this file once the entire codebase follows these rules.
|
||||
*/
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
"custom",
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
typescript: {},
|
||||
node: {
|
||||
moduleDirectory: ["node_modules", "."],
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
groups: ["builtin", "external", "internal", "parent", "sibling"],
|
||||
pathGroups: [
|
||||
{
|
||||
pattern: "react",
|
||||
group: "external",
|
||||
position: "before",
|
||||
},
|
||||
{
|
||||
pattern: "lucide-react",
|
||||
group: "external",
|
||||
position: "after",
|
||||
},
|
||||
{
|
||||
pattern: "@headlessui/**",
|
||||
group: "external",
|
||||
position: "after",
|
||||
},
|
||||
{
|
||||
pattern: "@plane/**",
|
||||
group: "external",
|
||||
position: "after",
|
||||
},
|
||||
{
|
||||
pattern: "@/**",
|
||||
group: "internal",
|
||||
},
|
||||
],
|
||||
pathGroupsExcludedImportTypes: ["builtin", "internal", "react"],
|
||||
alphabetize: {
|
||||
order: "asc",
|
||||
caseInsensitive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
// This tells ESLint to load the config from the package `eslint-config-custom`
|
||||
extends: ["custom"],
|
||||
settings: {
|
||||
next: {
|
||||
rootDir: ["web/", "space/", "admin/"],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
*.sh text eol=lf
|
||||
@@ -2,7 +2,7 @@ name: Bug report
|
||||
description: Create a bug report to help us improve Plane
|
||||
title: "[bug]: "
|
||||
labels: [🐛bug]
|
||||
assignees: [vihar, pushya22]
|
||||
assignees: [srinivaspendem, pushya22]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Feature request
|
||||
description: Suggest a feature to improve Plane
|
||||
title: "[feature]: "
|
||||
labels: [✨feature]
|
||||
assignees: [vihar, pushya22]
|
||||
assignees: [srinivaspendem, pushya22]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
name: "Build and Push Docker Image"
|
||||
description: "Reusable action for building and pushing Docker images"
|
||||
inputs:
|
||||
docker-username:
|
||||
description: "The Dockerhub username"
|
||||
required: true
|
||||
docker-token:
|
||||
description: "The Dockerhub Token"
|
||||
required: true
|
||||
|
||||
# Docker Image Options
|
||||
docker-image-owner:
|
||||
description: "The owner of the Docker image"
|
||||
required: true
|
||||
docker-image-name:
|
||||
description: "The name of the Docker image"
|
||||
required: true
|
||||
build-context:
|
||||
description: "The build context"
|
||||
required: true
|
||||
default: "."
|
||||
dockerfile-path:
|
||||
description: "The path to the Dockerfile"
|
||||
required: true
|
||||
build-args:
|
||||
description: "The build arguments"
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
# Buildx Options
|
||||
buildx-driver:
|
||||
description: "Buildx driver"
|
||||
required: true
|
||||
default: "docker-container"
|
||||
buildx-version:
|
||||
description: "Buildx version"
|
||||
required: true
|
||||
default: "latest"
|
||||
buildx-platforms:
|
||||
description: "Buildx platforms"
|
||||
required: true
|
||||
default: "linux/amd64"
|
||||
buildx-endpoint:
|
||||
description: "Buildx endpoint"
|
||||
required: true
|
||||
default: "default"
|
||||
|
||||
# Release Build Options
|
||||
build-release:
|
||||
description: "Flag to publish release"
|
||||
required: false
|
||||
default: "false"
|
||||
build-prerelease:
|
||||
description: "Flag to publish prerelease"
|
||||
required: false
|
||||
default: "false"
|
||||
release-version:
|
||||
description: "The release version"
|
||||
required: false
|
||||
default: "latest"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Set Docker Tag
|
||||
shell: bash
|
||||
env:
|
||||
IMG_OWNER: ${{ inputs.docker-image-owner }}
|
||||
IMG_NAME: ${{ inputs.docker-image-name }}
|
||||
BUILD_RELEASE: ${{ inputs.build-release }}
|
||||
IS_PRERELEASE: ${{ inputs.build-prerelease }}
|
||||
REL_VERSION: ${{ inputs.release-version }}
|
||||
run: |
|
||||
FLAT_BRANCH_VERSION=$(echo "${{ github.ref_name }}" | sed 's/[^a-zA-Z0-9.-]//g')
|
||||
|
||||
if [ "${{ env.BUILD_RELEASE }}" == "true" ]; then
|
||||
semver_regex="^v([0-9]+)\.([0-9]+)\.([0-9]+)(-[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)?$"
|
||||
if [[ ! ${{ env.REL_VERSION }} =~ $semver_regex ]]; then
|
||||
echo "Invalid Release Version Format : ${{ env.REL_VERSION }}"
|
||||
echo "Please provide a valid SemVer version"
|
||||
echo "e.g. v1.2.3 or v1.2.3-alpha-1"
|
||||
echo "Exiting the build process"
|
||||
exit 1 # Exit with status 1 to fail the step
|
||||
fi
|
||||
|
||||
TAG=${{ env.IMG_OWNER }}/${{ env.IMG_NAME }}:${{ env.REL_VERSION }}
|
||||
|
||||
if [ "${{ env.IS_PRERELEASE }}" != "true" ]; then
|
||||
TAG=${TAG},${{ env.IMG_OWNER }}/${{ env.IMG_NAME }}:stable
|
||||
fi
|
||||
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||
TAG=${{ env.IMG_OWNER }}/${{ env.IMG_NAME }}:latest
|
||||
else
|
||||
TAG=${{ env.IMG_OWNER }}/${{ env.IMG_NAME }}:${FLAT_BRANCH_VERSION}
|
||||
fi
|
||||
|
||||
echo "DOCKER_TAGS=${TAG}" >> $GITHUB_ENV
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ inputs.docker-username }}
|
||||
password: ${{ inputs.docker-token}}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver: ${{ inputs.buildx-driver }}
|
||||
version: ${{ inputs.buildx-version }}
|
||||
endpoint: ${{ inputs.buildx-endpoint }}
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and Push Docker Image
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: ${{ inputs.build-context }}
|
||||
file: ${{ inputs.dockerfile-path }}
|
||||
platforms: ${{ inputs.buildx-platforms }}
|
||||
tags: ${{ env.DOCKER_TAGS }}
|
||||
push: true
|
||||
build-args: ${{ inputs.build-args }}
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ inputs.docker-username }}
|
||||
DOCKER_PASSWORD: ${{ inputs.docker-token }}
|
||||
@@ -2,11 +2,6 @@ name: Build AIO Base Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
base_tag_name:
|
||||
description: 'Base Tag Name'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.ref_name }}
|
||||
@@ -27,29 +22,27 @@ jobs:
|
||||
- id: set_env_variables
|
||||
name: Set Environment Variables
|
||||
run: |
|
||||
echo "TARGET_BRANCH=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ "${{ github.event.inputs.base_tag_name }}" != "" ]; then
|
||||
echo "IMAGE_TAG=${{ github.event.inputs.base_tag_name }}" >> $GITHUB_OUTPUT
|
||||
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||
echo "IMAGE_TAG=latest" >> $GITHUB_OUTPUT
|
||||
elif [ "${{ env.TARGET_BRANCH }}" == "preview" ]; then
|
||||
echo "IMAGE_TAG=preview" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "IMAGE_TAG=develop" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
|
||||
if [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||
echo "BUILDX_DRIVER=cloud" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_VERSION=lab:latest" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_PLATFORMS=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_ENDPOINT=makeplane/plane-dev" >> $GITHUB_OUTPUT
|
||||
echo "TARGET_BRANCH=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "IMAGE_TAG=latest" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "BUILDX_DRIVER=docker-container" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_VERSION=latest" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_PLATFORMS=linux/amd64" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_ENDPOINT=" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ "${{ env.TARGET_BRANCH }}" == "preview" ]; then
|
||||
echo "TARGET_BRANCH=preview" >> $GITHUB_OUTPUT
|
||||
echo "IMAGE_TAG=preview" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "TARGET_BRANCH=develop" >> $GITHUB_OUTPUT
|
||||
echo "IMAGE_TAG=develop" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
|
||||
- id: checkout_files
|
||||
@@ -61,6 +54,7 @@ jobs:
|
||||
needs: [base_build_setup]
|
||||
env:
|
||||
BASE_IMG_TAG: makeplane/plane-aio-base:full-${{ needs.base_build_setup.outputs.image_tag }}
|
||||
TARGET_BRANCH: ${{ needs.base_build_setup.outputs.gh_branch_name }}
|
||||
BUILDX_DRIVER: ${{ needs.base_build_setup.outputs.gh_buildx_driver }}
|
||||
BUILDX_VERSION: ${{ needs.base_build_setup.outputs.gh_buildx_version }}
|
||||
BUILDX_PLATFORMS: ${{ needs.base_build_setup.outputs.gh_buildx_platforms }}
|
||||
@@ -83,7 +77,7 @@ jobs:
|
||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||
|
||||
- name: Build and Push to Docker Hub
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: ./aio
|
||||
file: ./aio/Dockerfile-base-full
|
||||
@@ -102,6 +96,7 @@ jobs:
|
||||
needs: [base_build_setup]
|
||||
env:
|
||||
BASE_IMG_TAG: makeplane/plane-aio-base:slim-${{ needs.base_build_setup.outputs.image_tag }}
|
||||
TARGET_BRANCH: ${{ needs.base_build_setup.outputs.gh_branch_name }}
|
||||
BUILDX_DRIVER: ${{ needs.base_build_setup.outputs.gh_buildx_driver }}
|
||||
BUILDX_VERSION: ${{ needs.base_build_setup.outputs.gh_buildx_version }}
|
||||
BUILDX_PLATFORMS: ${{ needs.base_build_setup.outputs.gh_buildx_platforms }}
|
||||
@@ -124,7 +119,7 @@ jobs:
|
||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||
|
||||
- name: Build and Push to Docker Hub
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: ./aio
|
||||
file: ./aio/Dockerfile-base-slim
|
||||
|
||||
@@ -13,10 +13,6 @@ on:
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
base_tag_name:
|
||||
description: 'Base Tag Name'
|
||||
required: false
|
||||
default: ''
|
||||
release:
|
||||
types: [released, prereleased]
|
||||
|
||||
@@ -31,7 +27,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }}
|
||||
flat_branch_name: ${{ steps.set_env_variables.outputs.FLAT_BRANCH_NAME }}
|
||||
gh_buildx_driver: ${{ steps.set_env_variables.outputs.BUILDX_DRIVER }}
|
||||
gh_buildx_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }}
|
||||
gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }}
|
||||
@@ -57,9 +52,7 @@ jobs:
|
||||
echo "BUILDX_PLATFORMS=linux/amd64" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_ENDPOINT=" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ "${{ github.event_name}}" == "workflow_dispatch" ] && [ "${{ github.event.inputs.base_tag_name }}" != "" ]; then
|
||||
echo "AIO_BASE_TAG=${{ github.event.inputs.base_tag_name }}" >> $GITHUB_OUTPUT
|
||||
elif [ "${{ env.TARGET_BRANCH }}" == "preview" ]; then
|
||||
if [ "${{ env.TARGET_BRANCH }}" == "preview" ]; then
|
||||
echo "AIO_BASE_TAG=preview" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "AIO_BASE_TAG=develop" >> $GITHUB_OUTPUT
|
||||
@@ -79,9 +72,6 @@ jobs:
|
||||
echo "DO_SLIM_BUILD=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
FLAT_BRANCH_NAME=$(echo "${{ env.TARGET_BRANCH }}" | sed 's/[^a-zA-Z0-9]/-/g')
|
||||
echo "FLAT_BRANCH_NAME=$FLAT_BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- id: checkout_files
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
@@ -93,7 +83,7 @@ jobs:
|
||||
env:
|
||||
BUILD_TYPE: full
|
||||
AIO_BASE_TAG: ${{ needs.branch_build_setup.outputs.aio_base_tag }}
|
||||
AIO_IMAGE_TAGS: makeplane/plane-aio:full-${{ needs.branch_build_setup.outputs.flat_branch_name }}
|
||||
AIO_IMAGE_TAGS: makeplane/plane-aio:full-${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
@@ -128,7 +118,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and Push to Docker Hub
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: .
|
||||
file: ./aio/Dockerfile-app
|
||||
@@ -136,7 +126,7 @@ jobs:
|
||||
tags: ${{ env.AIO_IMAGE_TAGS }}
|
||||
push: true
|
||||
build-args: |
|
||||
BASE_TAG=${{ env.AIO_BASE_TAG }}
|
||||
BUILD_TAG=${{ env.AIO_BASE_TAG }}
|
||||
BUILD_TYPE=${{env.BUILD_TYPE}}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -153,7 +143,7 @@ jobs:
|
||||
env:
|
||||
BUILD_TYPE: slim
|
||||
AIO_BASE_TAG: ${{ needs.branch_build_setup.outputs.aio_base_tag }}
|
||||
AIO_IMAGE_TAGS: makeplane/plane-aio:slim-${{ needs.branch_build_setup.outputs.flat_branch_name }}
|
||||
AIO_IMAGE_TAGS: makeplane/plane-aio:slim-${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
BUILDX_DRIVER: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
BUILDX_VERSION: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
@@ -188,7 +178,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and Push to Docker Hub
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: .
|
||||
file: ./aio/Dockerfile-app
|
||||
@@ -196,7 +186,7 @@ jobs:
|
||||
tags: ${{ env.AIO_IMAGE_TAGS }}
|
||||
push: true
|
||||
build-args: |
|
||||
BASE_TAG=${{ env.AIO_BASE_TAG }}
|
||||
BUILD_TAG=${{ env.AIO_BASE_TAG }}
|
||||
BUILD_TYPE=${{env.BUILD_TYPE}}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
+251
-291
@@ -1,42 +1,21 @@
|
||||
name: Branch Build CE
|
||||
name: Branch Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_type:
|
||||
description: "Type of build to run"
|
||||
required: true
|
||||
type: choice
|
||||
default: "Build"
|
||||
options:
|
||||
- "Build"
|
||||
- "Release"
|
||||
releaseVersion:
|
||||
description: "Release Version"
|
||||
type: string
|
||||
default: v0.0.0
|
||||
isPrerelease:
|
||||
description: "Is Pre-release"
|
||||
type: boolean
|
||||
default: false
|
||||
required: true
|
||||
arm64:
|
||||
description: "Build for ARM64 architecture"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- preview
|
||||
release:
|
||||
types: [released, prereleased]
|
||||
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.ref_name }}
|
||||
ARM64_BUILD: ${{ github.event.inputs.arm64 }}
|
||||
BUILD_TYPE: ${{ github.event.inputs.build_type }}
|
||||
RELEASE_VERSION: ${{ github.event.inputs.releaseVersion }}
|
||||
IS_PRERELEASE: ${{ github.event.inputs.isPrerelease }}
|
||||
TARGET_BRANCH: ${{ github.ref_name || github.event.release.target_commitish }}
|
||||
|
||||
jobs:
|
||||
branch_build_setup:
|
||||
name: Build Setup
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }}
|
||||
gh_buildx_driver: ${{ steps.set_env_variables.outputs.BUILDX_DRIVER }}
|
||||
@@ -48,25 +27,12 @@ jobs:
|
||||
build_admin: ${{ steps.changed_files.outputs.admin_any_changed }}
|
||||
build_space: ${{ steps.changed_files.outputs.space_any_changed }}
|
||||
build_web: ${{ steps.changed_files.outputs.web_any_changed }}
|
||||
build_live: ${{ steps.changed_files.outputs.live_any_changed }}
|
||||
|
||||
dh_img_web: ${{ steps.set_env_variables.outputs.DH_IMG_WEB }}
|
||||
dh_img_space: ${{ steps.set_env_variables.outputs.DH_IMG_SPACE }}
|
||||
dh_img_admin: ${{ steps.set_env_variables.outputs.DH_IMG_ADMIN }}
|
||||
dh_img_live: ${{ steps.set_env_variables.outputs.DH_IMG_LIVE }}
|
||||
dh_img_backend: ${{ steps.set_env_variables.outputs.DH_IMG_BACKEND }}
|
||||
dh_img_proxy: ${{ steps.set_env_variables.outputs.DH_IMG_PROXY }}
|
||||
|
||||
build_type: ${{steps.set_env_variables.outputs.BUILD_TYPE}}
|
||||
build_release: ${{ steps.set_env_variables.outputs.BUILD_RELEASE }}
|
||||
build_prerelease: ${{ steps.set_env_variables.outputs.BUILD_PRERELEASE }}
|
||||
release_version: ${{ steps.set_env_variables.outputs.RELEASE_VERSION }}
|
||||
|
||||
steps:
|
||||
- id: set_env_variables
|
||||
name: Set Environment Variables
|
||||
run: |
|
||||
if [ "${{ env.ARM64_BUILD }}" == "true" ] || ([ "${{ env.BUILD_TYPE }}" == "Release" ] && [ "${{ env.IS_PRERELEASE }}" != "true" ]); then
|
||||
if [ "${{ env.TARGET_BRANCH }}" == "master" ] || [ "${{ github.event_name }}" == "release" ]; then
|
||||
echo "BUILDX_DRIVER=cloud" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_VERSION=lab:latest" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_PLATFORMS=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
|
||||
@@ -77,43 +43,7 @@ jobs:
|
||||
echo "BUILDX_PLATFORMS=linux/amd64" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_ENDPOINT=" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
BR_NAME=$( echo "${{ env.TARGET_BRANCH }}" |sed 's/[^a-zA-Z0-9.-]//g')
|
||||
echo "TARGET_BRANCH=$BR_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "DH_IMG_WEB=plane-frontend" >> $GITHUB_OUTPUT
|
||||
echo "DH_IMG_SPACE=plane-space" >> $GITHUB_OUTPUT
|
||||
echo "DH_IMG_ADMIN=plane-admin" >> $GITHUB_OUTPUT
|
||||
echo "DH_IMG_LIVE=plane-live" >> $GITHUB_OUTPUT
|
||||
echo "DH_IMG_BACKEND=plane-backend" >> $GITHUB_OUTPUT
|
||||
echo "DH_IMG_PROXY=plane-proxy" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "BUILD_TYPE=${{env.BUILD_TYPE}}" >> $GITHUB_OUTPUT
|
||||
BUILD_RELEASE=false
|
||||
BUILD_PRERELEASE=false
|
||||
RELVERSION="latest"
|
||||
|
||||
if [ "${{ env.BUILD_TYPE }}" == "Release" ]; then
|
||||
FLAT_RELEASE_VERSION=$(echo "${{ env.RELEASE_VERSION }}" | sed 's/[^a-zA-Z0-9.-]//g')
|
||||
echo "FLAT_RELEASE_VERSION=${FLAT_RELEASE_VERSION}" >> $GITHUB_OUTPUT
|
||||
|
||||
semver_regex="^v([0-9]+)\.([0-9]+)\.([0-9]+)(-[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)?$"
|
||||
if [[ ! $FLAT_RELEASE_VERSION =~ $semver_regex ]]; then
|
||||
echo "Invalid Release Version Format : $FLAT_RELEASE_VERSION"
|
||||
echo "Please provide a valid SemVer version"
|
||||
echo "e.g. v1.2.3 or v1.2.3-alpha-1"
|
||||
echo "Exiting the build process"
|
||||
exit 1 # Exit with status 1 to fail the step
|
||||
fi
|
||||
BUILD_RELEASE=true
|
||||
RELVERSION=$FLAT_RELEASE_VERSION
|
||||
|
||||
if [ "${{ env.IS_PRERELEASE }}" == "true" ]; then
|
||||
BUILD_PRERELEASE=true
|
||||
fi
|
||||
fi
|
||||
echo "BUILD_RELEASE=${BUILD_RELEASE}" >> $GITHUB_OUTPUT
|
||||
echo "BUILD_PRERELEASE=${BUILD_PRERELEASE}" >> $GITHUB_OUTPUT
|
||||
echo "RELEASE_VERSION=${RELVERSION}" >> $GITHUB_OUTPUT
|
||||
echo "TARGET_BRANCH=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- id: checkout_files
|
||||
name: Checkout Files
|
||||
@@ -131,251 +61,281 @@ jobs:
|
||||
admin:
|
||||
- admin/**
|
||||
- packages/**
|
||||
- "package.json"
|
||||
- "yarn.lock"
|
||||
- "tsconfig.json"
|
||||
- "turbo.json"
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
- 'tsconfig.json'
|
||||
- 'turbo.json'
|
||||
space:
|
||||
- space/**
|
||||
- packages/**
|
||||
- "package.json"
|
||||
- "yarn.lock"
|
||||
- "tsconfig.json"
|
||||
- "turbo.json"
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
- 'tsconfig.json'
|
||||
- 'turbo.json'
|
||||
web:
|
||||
- web/**
|
||||
- packages/**
|
||||
- "package.json"
|
||||
- "yarn.lock"
|
||||
- "tsconfig.json"
|
||||
- "turbo.json"
|
||||
live:
|
||||
- live/**
|
||||
- packages/**
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
- 'tsconfig.json'
|
||||
- 'turbo.json'
|
||||
|
||||
branch_build_push_admin:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_admin == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
name: Build-Push Admin Docker Image
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
steps:
|
||||
- id: checkout_files
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
- name: Admin Build and Push
|
||||
uses: ./.github/actions/build-push-ce
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
|
||||
docker-username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker-token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
docker-image-owner: makeplane
|
||||
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_admin }}
|
||||
build-context: .
|
||||
dockerfile-path: ./admin/Dockerfile.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 }}
|
||||
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
|
||||
branch_build_push_web:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_web == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
name: Build-Push Web Docker Image
|
||||
if: ${{ needs.branch_build_setup.outputs.build_web == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
FRONTEND_TAG: makeplane/plane-frontend:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
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 }}
|
||||
BUILDX_ENDPOINT: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
steps:
|
||||
- id: checkout_files
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
- name: Web Build and Push
|
||||
uses: ./.github/actions/build-push-ce
|
||||
- name: Set Frontend Docker Tag
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "release" ]; then
|
||||
TAG=makeplane/plane-frontend:stable,makeplane/plane-frontend:${{ github.event.release.tag_name }}
|
||||
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||
TAG=makeplane/plane-frontend:latest
|
||||
else
|
||||
TAG=${{ env.FRONTEND_TAG }}
|
||||
fi
|
||||
echo "FRONTEND_TAG=${TAG}" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
|
||||
docker-username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker-token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
docker-image-owner: makeplane
|
||||
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_web }}
|
||||
build-context: .
|
||||
dockerfile-path: ./web/Dockerfile.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 }}
|
||||
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver: ${{ env.BUILDX_DRIVER }}
|
||||
version: ${{ env.BUILDX_VERSION }}
|
||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and Push Frontend to Docker Container Registry
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: .
|
||||
file: ./web/Dockerfile.web
|
||||
platforms: ${{ env.BUILDX_PLATFORMS }}
|
||||
tags: ${{ env.FRONTEND_TAG }}
|
||||
push: true
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
branch_build_push_admin:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_admin== 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
ADMIN_TAG: makeplane/plane-admin:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
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 }}
|
||||
BUILDX_ENDPOINT: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
steps:
|
||||
- name: Set Admin Docker Tag
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "release" ]; then
|
||||
TAG=makeplane/plane-admin:stable,makeplane/plane-admin:${{ github.event.release.tag_name }}
|
||||
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||
TAG=makeplane/plane-admin:latest
|
||||
else
|
||||
TAG=${{ env.ADMIN_TAG }}
|
||||
fi
|
||||
echo "ADMIN_TAG=${TAG}" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver: ${{ env.BUILDX_DRIVER }}
|
||||
version: ${{ env.BUILDX_VERSION }}
|
||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and Push Frontend to Docker Container Registry
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: .
|
||||
file: ./admin/Dockerfile.admin
|
||||
platforms: ${{ env.BUILDX_PLATFORMS }}
|
||||
tags: ${{ env.ADMIN_TAG }}
|
||||
push: true
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
branch_build_push_space:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
name: Build-Push Space Docker Image
|
||||
if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
SPACE_TAG: makeplane/plane-space:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
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 }}
|
||||
BUILDX_ENDPOINT: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
steps:
|
||||
- id: checkout_files
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
- name: Space Build and Push
|
||||
uses: ./.github/actions/build-push-ce
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
|
||||
docker-username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker-token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
docker-image-owner: makeplane
|
||||
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_space }}
|
||||
build-context: .
|
||||
dockerfile-path: ./space/Dockerfile.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 }}
|
||||
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
- name: Set Space Docker Tag
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "release" ]; then
|
||||
TAG=makeplane/plane-space:stable,makeplane/plane-space:${{ github.event.release.tag_name }}
|
||||
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||
TAG=makeplane/plane-space:latest
|
||||
else
|
||||
TAG=${{ env.SPACE_TAG }}
|
||||
fi
|
||||
echo "SPACE_TAG=${TAG}" >> $GITHUB_ENV
|
||||
|
||||
branch_build_push_live:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_live == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
name: Build-Push Live Collaboration Docker Image
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
steps:
|
||||
- id: checkout_files
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
- name: Live Build and Push
|
||||
uses: ./.github/actions/build-push-ce
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
|
||||
docker-username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker-token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
docker-image-owner: makeplane
|
||||
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_live }}
|
||||
build-context: .
|
||||
dockerfile-path: ./live/Dockerfile.live
|
||||
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 }}
|
||||
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver: ${{ env.BUILDX_DRIVER }}
|
||||
version: ${{ env.BUILDX_VERSION }}
|
||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and Push Space to Docker Hub
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: .
|
||||
file: ./space/Dockerfile.space
|
||||
platforms: ${{ env.BUILDX_PLATFORMS }}
|
||||
tags: ${{ env.SPACE_TAG }}
|
||||
push: true
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
branch_build_push_apiserver:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_apiserver == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
name: Build-Push API Server Docker Image
|
||||
if: ${{ needs.branch_build_setup.outputs.build_apiserver == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
env:
|
||||
BACKEND_TAG: makeplane/plane-backend:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
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 }}
|
||||
BUILDX_ENDPOINT: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
steps:
|
||||
- id: checkout_files
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
- name: Backend Build and Push
|
||||
uses: ./.github/actions/build-push-ce
|
||||
- name: Set Backend Docker Tag
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "release" ]; then
|
||||
TAG=makeplane/plane-backend:stable,makeplane/plane-backend:${{ github.event.release.tag_name }}
|
||||
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||
TAG=makeplane/plane-backend:latest
|
||||
else
|
||||
TAG=${{ env.BACKEND_TAG }}
|
||||
fi
|
||||
echo "BACKEND_TAG=${TAG}" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
|
||||
docker-username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker-token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
docker-image-owner: makeplane
|
||||
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_backend }}
|
||||
build-context: ./apiserver
|
||||
dockerfile-path: ./apiserver/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 }}
|
||||
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver: ${{ env.BUILDX_DRIVER }}
|
||||
version: ${{ env.BUILDX_VERSION }}
|
||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and Push Backend to Docker Hub
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: ./apiserver
|
||||
file: ./apiserver/Dockerfile.api
|
||||
platforms: ${{ env.BUILDX_PLATFORMS }}
|
||||
push: true
|
||||
tags: ${{ env.BACKEND_TAG }}
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
branch_build_push_proxy:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
name: Build-Push Proxy Docker Image
|
||||
if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
steps:
|
||||
- id: checkout_files
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
- name: Proxy Build and Push
|
||||
uses: ./.github/actions/build-push-ce
|
||||
with:
|
||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
|
||||
docker-username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker-token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
docker-image-owner: makeplane
|
||||
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_proxy }}
|
||||
build-context: ./nginx
|
||||
dockerfile-path: ./nginx/Dockerfile
|
||||
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 }}
|
||||
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
|
||||
attach_assets_to_build:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_type == 'Build' }}
|
||||
name: Attach Assets to Build
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [branch_build_setup]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Update Assets
|
||||
run: |
|
||||
cp ./deploy/selfhost/install.sh deploy/selfhost/setup.sh
|
||||
|
||||
- name: Attach Assets
|
||||
id: attach_assets
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: selfhost-assets
|
||||
retention-days: 2
|
||||
path: |
|
||||
${{ github.workspace }}/deploy/selfhost/setup.sh
|
||||
${{ github.workspace }}/deploy/selfhost/restore.sh
|
||||
${{ github.workspace }}/deploy/selfhost/docker-compose.yml
|
||||
${{ github.workspace }}/deploy/selfhost/variables.env
|
||||
|
||||
publish_release:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_type == 'Release' }}
|
||||
name: Build Release
|
||||
runs-on: ubuntu-20.04
|
||||
needs:
|
||||
[
|
||||
branch_build_setup,
|
||||
branch_build_push_admin,
|
||||
branch_build_push_web,
|
||||
branch_build_push_space,
|
||||
branch_build_push_live,
|
||||
branch_build_push_apiserver,
|
||||
branch_build_push_proxy,
|
||||
attach_assets_to_build,
|
||||
]
|
||||
env:
|
||||
REL_VERSION: ${{ needs.branch_build_setup.outputs.release_version }}
|
||||
PROXY_TAG: makeplane/plane-proxy:${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
TARGET_BRANCH: ${{ needs.branch_build_setup.outputs.gh_branch_name }}
|
||||
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 }}
|
||||
BUILDX_ENDPOINT: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
- name: Set Proxy Docker Tag
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "release" ]; then
|
||||
TAG=makeplane/plane-proxy:stable,makeplane/plane-proxy:${{ github.event.release.tag_name }}
|
||||
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||
TAG=makeplane/plane-proxy:latest
|
||||
else
|
||||
TAG=${{ env.PROXY_TAG }}
|
||||
fi
|
||||
echo "PROXY_TAG=${TAG}" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver: ${{ env.BUILDX_DRIVER }}
|
||||
version: ${{ env.BUILDX_VERSION }}
|
||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Update Assets
|
||||
run: |
|
||||
cp ./deploy/selfhost/install.sh deploy/selfhost/setup.sh
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v2.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
|
||||
- name: Build and Push Plane-Proxy to Docker Hub
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
tag_name: ${{ env.REL_VERSION }}
|
||||
name: ${{ env.REL_VERSION }}
|
||||
draft: false
|
||||
prerelease: ${{ env.IS_PRERELEASE }}
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
${{ github.workspace }}/deploy/selfhost/setup.sh
|
||||
${{ github.workspace }}/deploy/selfhost/restore.sh
|
||||
${{ github.workspace }}/deploy/selfhost/docker-compose.yml
|
||||
${{ github.workspace }}/deploy/selfhost/variables.env
|
||||
context: ./nginx
|
||||
file: ./nginx/Dockerfile
|
||||
platforms: ${{ env.BUILDX_PLATFORMS }}
|
||||
tags: ${{ env.PROXY_TAG }}
|
||||
push: true
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
@@ -29,11 +29,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -59,6 +59,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -8,20 +8,37 @@ on:
|
||||
|
||||
env:
|
||||
CURRENT_BRANCH: ${{ github.ref_name }}
|
||||
TARGET_BRANCH: "preview" # The target branch that you would like to merge changes like develop
|
||||
SOURCE_BRANCH: ${{ vars.SYNC_SOURCE_BRANCH_NAME }} # The sync branch such as "sync/ce"
|
||||
TARGET_BRANCH: ${{ vars.SYNC_TARGET_BRANCH_NAME }} # The target branch that you would like to merge changes like develop
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} # Personal access token required to modify contents and workflows
|
||||
REVIEWER: ${{ vars.SYNC_PR_REVIEWER }}
|
||||
ACCOUNT_USER_NAME: ${{ vars.ACCOUNT_USER_NAME }}
|
||||
ACCOUNT_USER_EMAIL: ${{ vars.ACCOUNT_USER_EMAIL }}
|
||||
|
||||
jobs:
|
||||
create_pull_request:
|
||||
Check_Branch:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
BRANCH_MATCH: ${{ steps.check-branch.outputs.MATCH }}
|
||||
steps:
|
||||
- name: Check if current branch matches the secret
|
||||
id: check-branch
|
||||
run: |
|
||||
if [ "$CURRENT_BRANCH" = "$SOURCE_BRANCH" ]; then
|
||||
echo "MATCH=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "MATCH=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
Create_PR:
|
||||
if: ${{ needs.Check_Branch.outputs.BRANCH_MATCH == 'true' }}
|
||||
needs: [Check_Branch]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for all branches and tags
|
||||
|
||||
@@ -42,11 +59,11 @@ jobs:
|
||||
- name: Create PR to Target Branch
|
||||
run: |
|
||||
# get all pull requests and check if there is already a PR
|
||||
PR_EXISTS=$(gh pr list --base $TARGET_BRANCH --head $CURRENT_BRANCH --state open --json number | jq '.[] | .number')
|
||||
PR_EXISTS=$(gh pr list --base $TARGET_BRANCH --head $SOURCE_BRANCH --state open --json number | jq '.[] | .number')
|
||||
if [ -n "$PR_EXISTS" ]; then
|
||||
echo "Pull Request already exists: $PR_EXISTS"
|
||||
else
|
||||
echo "Creating new pull request"
|
||||
PR_URL=$(gh pr create --base $TARGET_BRANCH --head $CURRENT_BRANCH --title "${{ vars.SYNC_PR_TITLE }}" --body "")
|
||||
PR_URL=$(gh pr create --base $TARGET_BRANCH --head $SOURCE_BRANCH --title "sync: community changes" --body "")
|
||||
echo "Pull Request created: $PR_URL"
|
||||
fi
|
||||
@@ -2,11 +2,6 @@ name: Feature Preview
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
base_tag_name:
|
||||
description: 'Base Tag Name'
|
||||
required: false
|
||||
default: 'preview'
|
||||
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.ref_name }}
|
||||
@@ -34,12 +29,7 @@ jobs:
|
||||
echo "BUILDX_VERSION=latest" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_PLATFORMS=linux/amd64" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_ENDPOINT=" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ "${{ github.event.inputs.base_tag_name }}" != "" ]; then
|
||||
echo "AIO_BASE_TAG=${{ github.event.inputs.base_tag_name }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "AIO_BASE_TAG=develop" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
echo "AIO_BASE_TAG=develop" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "TARGET_BRANCH=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -79,7 +69,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and Push to Docker Hub
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: .
|
||||
file: ./aio/Dockerfile-app
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -1,16 +0,0 @@
|
||||
{ pkgs, ... }: {
|
||||
|
||||
# Which nixpkgs channel to use.
|
||||
channel = "stable-23.11"; # or "unstable"
|
||||
|
||||
# Use https://search.nixos.org/packages to find packages
|
||||
packages = [
|
||||
pkgs.nodejs_20
|
||||
pkgs.python3
|
||||
];
|
||||
|
||||
services.docker.enable = true;
|
||||
services.postgres.enable = true;
|
||||
services.redis.enable = true;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"*.{ts,tsx,js,jsx}": ["eslint -c ./.eslintrc-staged.js", "prettier --check"]
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@ Thank you for showing an interest in contributing to Plane! All kinds of contrib
|
||||
|
||||
## Submitting an issue
|
||||
|
||||
Before submitting a new issue, please search the [issues](https://github.com/makeplane/plane/issues) tab. Maybe an issue or discussion already exists and might inform you of workarounds. Otherwise, you can give new information.
|
||||
Before submitting a new issue, please search the [issues](https://github.com/makeplane/plane/issues) tab. Maybe an issue or discussion already exists and might inform you of workarounds. Otherwise, you can give new informplaneation.
|
||||
|
||||
While we want to fix all the [issues](https://github.com/makeplane/plane/issues), before fixing a bug we need to be able to reproduce and confirm it. Please provide us with a minimal reproduction scenario using a repository or [Gist](https://gist.github.com/). Having a live, reproducible scenario gives us the information without asking questions back & forth with additional questions like:
|
||||
|
||||
|
||||
+62
-23
@@ -1,5 +1,6 @@
|
||||
# Environment Variables
|
||||
|
||||
|
||||
Environment variables are distributed in various files. Please refer them carefully.
|
||||
|
||||
## {PROJECT_FOLDER}/.env
|
||||
@@ -8,13 +9,17 @@ File is available in the project root folder
|
||||
|
||||
```
|
||||
# Database Settings
|
||||
POSTGRES_USER="plane"
|
||||
POSTGRES_PASSWORD="plane"
|
||||
POSTGRES_DB="plane"
|
||||
PGDATA="/var/lib/postgresql/data"
|
||||
PGUSER="plane"
|
||||
PGPASSWORD="plane"
|
||||
PGHOST="plane-db"
|
||||
PGDATABASE="plane"
|
||||
DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
|
||||
|
||||
# Redis Settings
|
||||
REDIS_HOST="plane-redis"
|
||||
REDIS_PORT="6379"
|
||||
REDIS_URL="redis://${REDIS_HOST}:6379/"
|
||||
|
||||
# AWS Settings
|
||||
AWS_REGION=""
|
||||
AWS_ACCESS_KEY_ID="access-key"
|
||||
@@ -24,39 +29,63 @@ AWS_S3_ENDPOINT_URL="http://plane-minio:9000"
|
||||
AWS_S3_BUCKET_NAME="uploads"
|
||||
# Maximum file upload limit
|
||||
FILE_SIZE_LIMIT=5242880
|
||||
|
||||
# GPT settings
|
||||
OPENAI_API_BASE="https://api.openai.com/v1" # deprecated
|
||||
OPENAI_API_KEY="sk-" # deprecated
|
||||
GPT_ENGINE="gpt-3.5-turbo" # deprecated
|
||||
# Settings related to Docker
|
||||
DOCKERIZED=1 # deprecated
|
||||
|
||||
# set to 1 If using the pre-configured minio setup
|
||||
USE_MINIO=1
|
||||
|
||||
# Nginx Configuration
|
||||
NGINX_PORT=80
|
||||
```
|
||||
|
||||
|
||||
|
||||
## {PROJECT_FOLDER}/web/.env.example
|
||||
|
||||
|
||||
|
||||
```
|
||||
# Public boards deploy URL
|
||||
NEXT_PUBLIC_DEPLOY_URL="http://localhost/spaces"
|
||||
```
|
||||
|
||||
## {PROJECT_FOLDER}/apiserver/.env
|
||||
|
||||
|
||||
|
||||
```
|
||||
# Backend
|
||||
# Debug value for api server use it as 0 for production use
|
||||
DEBUG=0
|
||||
CORS_ALLOWED_ORIGINS="http://localhost"
|
||||
|
||||
# Error logs
|
||||
SENTRY_DSN=""
|
||||
SENTRY_ENVIRONMENT="development"
|
||||
|
||||
# Database Settings
|
||||
POSTGRES_USER="plane"
|
||||
POSTGRES_PASSWORD="plane"
|
||||
POSTGRES_HOST="plane-db"
|
||||
POSTGRES_DB="plane"
|
||||
POSTGRES_PORT=5432
|
||||
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
|
||||
PGUSER="plane"
|
||||
PGPASSWORD="plane"
|
||||
PGHOST="plane-db"
|
||||
PGDATABASE="plane"
|
||||
DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
|
||||
|
||||
# Redis Settings
|
||||
REDIS_HOST="plane-redis"
|
||||
REDIS_PORT="6379"
|
||||
REDIS_URL="redis://${REDIS_HOST}:6379/"
|
||||
|
||||
# Email Settings
|
||||
EMAIL_HOST=""
|
||||
EMAIL_HOST_USER=""
|
||||
EMAIL_HOST_PASSWORD=""
|
||||
EMAIL_PORT=587
|
||||
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
|
||||
EMAIL_USE_TLS="1"
|
||||
EMAIL_USE_SSL="0"
|
||||
|
||||
# AWS Settings
|
||||
AWS_REGION=""
|
||||
AWS_ACCESS_KEY_ID="access-key"
|
||||
@@ -66,25 +95,35 @@ AWS_S3_ENDPOINT_URL="http://plane-minio:9000"
|
||||
AWS_S3_BUCKET_NAME="uploads"
|
||||
# Maximum file upload limit
|
||||
FILE_SIZE_LIMIT=5242880
|
||||
|
||||
# GPT settings
|
||||
OPENAI_API_BASE="https://api.openai.com/v1" # deprecated
|
||||
OPENAI_API_KEY="sk-" # deprecated
|
||||
GPT_ENGINE="gpt-3.5-turbo" # deprecated
|
||||
|
||||
# Settings related to Docker
|
||||
DOCKERIZED=1 # deprecated
|
||||
DOCKERIZED=1 # Deprecated
|
||||
|
||||
# Github
|
||||
GITHUB_CLIENT_SECRET="" # For fetching release notes
|
||||
|
||||
# set to 1 If using the pre-configured minio setup
|
||||
USE_MINIO=1
|
||||
|
||||
# Nginx Configuration
|
||||
NGINX_PORT=80
|
||||
# Email redirections and minio domain settings
|
||||
|
||||
|
||||
# SignUps
|
||||
ENABLE_SIGNUP="1"
|
||||
|
||||
# Email Redirection URL
|
||||
WEB_URL="http://localhost"
|
||||
# Gunicorn Workers
|
||||
GUNICORN_WORKERS=2
|
||||
# Base URLs
|
||||
ADMIN_BASE_URL=
|
||||
SPACE_BASE_URL=
|
||||
APP_BASE_URL=
|
||||
SECRET_KEY="gxoytl7dmnc1y37zahah820z5iq3iozu38cnfjtu3yaau9cd9z"
|
||||
```
|
||||
|
||||
## Updates
|
||||
|
||||
- The environment variable NEXT_PUBLIC_API_BASE_URL has been removed from both the web and space projects.
|
||||
- The naming convention for containers and images has been updated.
|
||||
- The plane-worker image will no longer be maintained, as it has been merged with plane-backend.
|
||||
- The Tiptap pro-extension dependency has been removed, eliminating the need for Tiptap API keys.
|
||||
|
||||
+35
-30
@@ -1,39 +1,44 @@
|
||||
# Security policy
|
||||
This document outlines the security protocols and vulnerability reporting guidelines for the Plane project. Ensuring the security of our systems is a top priority, and while we work diligently to maintain robust protection, vulnerabilities may still occur. We highly value the community’s role in identifying and reporting security concerns to uphold the integrity of our systems and safeguard our users.
|
||||
# Security Policy
|
||||
|
||||
## Reporting a vulnerability
|
||||
If you have identified a security vulnerability, submit your findings to [security@plane.so](mailto:security@plane.so).
|
||||
Ensure your report includes all relevant information needed for us to reproduce and assess the issue. Include the IP address or URL of the affected system.
|
||||
This document outlines security procedures and vulnerabilities reporting for the Plane project.
|
||||
|
||||
To ensure a responsible and effective disclosure process, please adhere to the following:
|
||||
At Plane, we safeguarding the security of our systems with top priority. Despite our efforts, vulnerabilities may still exist. We greatly appreciate your assistance in identifying and reporting any such vulnerabilities to help us maintain the integrity of our systems and protect our clients.
|
||||
|
||||
- Maintain confidentiality and refrain from publicly disclosing the vulnerability until we have had the opportunity to investigate and address the issue.
|
||||
- Refrain from running automated vulnerability scans on our infrastructure or dashboard without prior consent. Contact us to set up a sandbox environment if necessary.
|
||||
- Do not exploit any discovered vulnerabilities for malicious purposes, such as accessing or altering user data.
|
||||
- Do not engage in physical security attacks, social engineering, distributed denial of service (DDoS) attacks, spam campaigns, or attacks on third-party applications as part of your vulnerability testing.
|
||||
To report a security vulnerability, please email us directly at security@plane.so with a detailed description of the vulnerability and steps to reproduce it. Please refrain from disclosing the vulnerability publicly until we have had an opportunity to review and address it.
|
||||
|
||||
## Out of scope
|
||||
While we appreciate all efforts to assist in improving our security, please note that the following types of vulnerabilities are considered out of scope:
|
||||
## Out of Scope Vulnerabilities
|
||||
|
||||
- Vulnerabilities requiring man-in-the-middle (MITM) attacks or physical access to a user’s device.
|
||||
- Content spoofing or text injection issues without a clear attack vector or the ability to modify HTML/CSS.
|
||||
- Issues related to email spoofing.
|
||||
- Missing DNSSEC, CAA, or CSP headers.
|
||||
- Absence of secure or HTTP-only flags on non-sensitive cookies.
|
||||
We appreciate your help in identifying vulnerabilities. However, please note that the following types of vulnerabilities are considered out of scope:
|
||||
|
||||
## Our commitment
|
||||
- Attacks requiring MITM or physical access to a user's device.
|
||||
- Content spoofing and text injection issues without demonstrating an attack vector or ability to modify HTML/CSS.
|
||||
- Email spoofing.
|
||||
- Missing DNSSEC, CAA, CSP headers.
|
||||
- Lack of Secure or HTTP only flag on non-sensitive cookies.
|
||||
|
||||
At Plane, we are committed to maintaining transparent and collaborative communication throughout the vulnerability resolution process. Here's what you can expect from us:
|
||||
## Reporting Process
|
||||
|
||||
- **Response Time** <br/>
|
||||
We will acknowledge receipt of your vulnerability report within three business days and provide an estimated timeline for resolution.
|
||||
- **Legal Protection** <br/>
|
||||
We will not initiate legal action against you for reporting vulnerabilities, provided you adhere to the reporting guidelines.
|
||||
- **Confidentiality** <br/>
|
||||
Your report will be treated with confidentiality. We will not disclose your personal information to third parties without your consent.
|
||||
- **Recognition** <br/>
|
||||
With your permission, we are happy to publicly acknowledge your contribution to improving our security once the issue is resolved.
|
||||
- **Timely Resolution** <br/>
|
||||
We are committed to working closely with you throughout the resolution process, providing timely updates as necessary. Our goal is to address all reported vulnerabilities swiftly, and we will actively engage with you to coordinate a responsible disclosure once the issue is fully resolved.
|
||||
If you discover a vulnerability, please adhere to the following reporting process:
|
||||
|
||||
We appreciate your help in ensuring the security of our platform. Your contributions are crucial to protecting our users and maintaining a secure environment. Thank you for working with us to keep Plane safe.
|
||||
1. Email your findings to security@plane.so.
|
||||
2. Refrain from running automated scanners on our infrastructure or dashboard without prior consent. Contact us to set up a sandbox environment if necessary.
|
||||
3. Do not exploit the vulnerability for malicious purposes, such as downloading excessive data or altering user data.
|
||||
4. Maintain confidentiality and refrain from disclosing the vulnerability until it has been resolved.
|
||||
5. Avoid using physical security attacks, social engineering, distributed denial of service, spam, or third-party applications.
|
||||
|
||||
When reporting a vulnerability, please provide sufficient information to allow us to reproduce and address the issue promptly. Include the IP address or URL of the affected system, along with a detailed description of the vulnerability.
|
||||
|
||||
## Our Commitment
|
||||
|
||||
We are committed to promptly addressing reported vulnerabilities and maintaining open communication throughout the resolution process. Here's what you can expect from us:
|
||||
|
||||
- **Response Time:** We will acknowledge receipt of your report within three business days and provide an expected resolution date.
|
||||
- **Legal Protection:** We will not pursue legal action against you for reporting vulnerabilities, provided you adhere to the reporting guidelines.
|
||||
- **Confidentiality:** Your report will be treated with strict confidentiality. We will not disclose your personal information to third parties without your consent.
|
||||
- **Progress Updates:** We will keep you informed of our progress in resolving the reported vulnerability.
|
||||
- **Recognition:** With your permission, we will publicly acknowledge you as the discoverer of the vulnerability.
|
||||
- **Timely Resolution:** We strive to resolve all reported vulnerabilities promptly and will actively participate in the publication process once the issue is resolved.
|
||||
|
||||
We appreciate your cooperation in helping us maintain the security of our systems and protecting our clients. Thank you for your contributions to our security efforts.
|
||||
|
||||
reference: https://supabase.com/.well-known/security.txt
|
||||
|
||||
+48
-4
@@ -1,8 +1,52 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/next.js"],
|
||||
extends: ["custom"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: true,
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
typescript: {},
|
||||
node: {
|
||||
moduleDirectory: ["node_modules", "."],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
rules: {
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
groups: ["builtin", "external", "internal", "parent", "sibling",],
|
||||
pathGroups: [
|
||||
{
|
||||
pattern: "react",
|
||||
group: "external",
|
||||
position: "before",
|
||||
},
|
||||
{
|
||||
pattern: "lucide-react",
|
||||
group: "external",
|
||||
position: "after",
|
||||
},
|
||||
{
|
||||
pattern: "@headlessui/**",
|
||||
group: "external",
|
||||
position: "after",
|
||||
},
|
||||
{
|
||||
pattern: "@plane/**",
|
||||
group: "external",
|
||||
position: "after",
|
||||
},
|
||||
{
|
||||
pattern: "@/**",
|
||||
group: "internal",
|
||||
}
|
||||
],
|
||||
pathGroupsExcludedImportTypes: ["builtin", "internal", "react"],
|
||||
alphabetize: {
|
||||
order: "asc",
|
||||
caseInsensitive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -121,12 +121,7 @@ export const InstanceAIForm: FC<IInstanceAIForm> = (props) => {
|
||||
|
||||
<div className="relative inline-flex items-center gap-2 rounded border border-custom-primary-100/20 bg-custom-primary-100/10 px-4 py-2 text-xs text-custom-primary-200">
|
||||
<Lightbulb height="14" width="14" />
|
||||
<div>
|
||||
If you have a preferred AI models vendor, please get in{" "}
|
||||
<a className="underline font-medium" href="https://plane.so/contact">
|
||||
touch with us.
|
||||
</a>
|
||||
</div>
|
||||
<div>If you have a preferred AI models vendor, please get in touch with us.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -195,7 +195,7 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
|
||||
</Button>
|
||||
<Link
|
||||
href="/authentication"
|
||||
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
|
||||
className={cn(getButtonStyling("link-neutral", "md"), "font-medium")}
|
||||
onClick={handleGoBack}
|
||||
>
|
||||
Go back
|
||||
|
||||
@@ -191,7 +191,7 @@ export const InstanceGitlabConfigForm: FC<Props> = (props) => {
|
||||
</Button>
|
||||
<Link
|
||||
href="/authentication"
|
||||
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
|
||||
className={cn(getButtonStyling("link-neutral", "md"), "font-medium")}
|
||||
onClick={handleGoBack}
|
||||
>
|
||||
Go back
|
||||
|
||||
@@ -192,7 +192,7 @@ export const InstanceGoogleConfigForm: FC<Props> = (props) => {
|
||||
</Button>
|
||||
<Link
|
||||
href="/authentication"
|
||||
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
|
||||
className={cn(getButtonStyling("link-neutral", "md"), "font-medium")}
|
||||
onClick={handleGoBack}
|
||||
>
|
||||
Go back
|
||||
|
||||
@@ -60,7 +60,7 @@ const InstanceAuthenticationPage = observer(() => {
|
||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-xl font-medium text-custom-text-100">Manage authentication modes for your instance</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
Configure authentication modes for your team and restrict sign-ups to be invite only.
|
||||
Configure authentication modes for your team and restrict sign ups to be invite only.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
||||
@@ -80,11 +80,9 @@ const InstanceAuthenticationPage = observer(() => {
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableSignUpConfig))}
|
||||
onChange={() => {
|
||||
if (Boolean(parseInt(enableSignUpConfig)) === true) {
|
||||
updateConfig("ENABLE_SIGNUP", "0");
|
||||
} else {
|
||||
updateConfig("ENABLE_SIGNUP", "1");
|
||||
}
|
||||
Boolean(parseInt(enableSignUpConfig)) === true
|
||||
? updateConfig("ENABLE_SIGNUP", "0")
|
||||
: updateConfig("ENABLE_SIGNUP", "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={isSubmitting}
|
||||
@@ -92,7 +90,7 @@ const InstanceAuthenticationPage = observer(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-lg font-medium pt-6">Available authentication modes</div>
|
||||
<div className="text-lg font-medium pt-6">Authentication modes</div>
|
||||
<AuthenticationModes disabled={isSubmitting} updateConfig={updateConfig} />
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -72,7 +72,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
{
|
||||
key: "EMAIL_FROM",
|
||||
type: "text",
|
||||
label: "Sender's email address",
|
||||
label: "Sender email address",
|
||||
description:
|
||||
"This is the email address your users will see when getting emails from this instance. You will need to verify this address.",
|
||||
placeholder: "no-reply@projectplane.so",
|
||||
@@ -174,12 +174,12 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 my-6 pt-4 border-t border-custom-border-100">
|
||||
<div className="flex w-full max-w-xl flex-col gap-y-10 px-1">
|
||||
<div className="flex w-full max-w-md flex-col gap-y-10 px-1">
|
||||
<div className="mr-8 flex items-center gap-10 pt-4">
|
||||
<div className="grow">
|
||||
<div className="text-sm font-medium text-custom-text-100">Authentication</div>
|
||||
<div className="text-sm font-medium text-custom-text-100">Authentication (optional)</div>
|
||||
<div className="text-xs font-normal text-custom-text-300">
|
||||
This is optional, but we recommend setting up a username and a password for your SMTP server.
|
||||
We recommend setting up a username password for your SMTP server
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,9 +9,8 @@ import { IInstance, IInstanceAdmin } from "@plane/types";
|
||||
import { Button, Input, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { ControllerInput } from "@/components/common";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
import { IntercomConfig } from "./intercom";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
export interface IGeneralConfigurationForm {
|
||||
instance: IInstance;
|
||||
@@ -21,13 +20,11 @@ export interface IGeneralConfigurationForm {
|
||||
export const GeneralConfigurationForm: FC<IGeneralConfigurationForm> = observer((props) => {
|
||||
const { instance, instanceAdmins } = props;
|
||||
// hooks
|
||||
const { instanceConfigurations, updateInstanceInfo, updateInstanceConfigurations } = useInstance();
|
||||
|
||||
const { updateInstanceInfo } = useInstance();
|
||||
// form data
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
watch,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<Partial<IInstance>>({
|
||||
defaultValues: {
|
||||
@@ -39,16 +36,7 @@ export const GeneralConfigurationForm: FC<IGeneralConfigurationForm> = observer(
|
||||
const onSubmit = async (formData: Partial<IInstance>) => {
|
||||
const payload: Partial<IInstance> = { ...formData };
|
||||
|
||||
// update the intercom configuration
|
||||
const isIntercomEnabled =
|
||||
instanceConfigurations?.find((config) => config.key === "IS_INTERCOM_ENABLED")?.value === "1";
|
||||
if (!payload.is_telemetry_enabled && isIntercomEnabled) {
|
||||
try {
|
||||
await updateInstanceConfigurations({ IS_INTERCOM_ENABLED: "0" });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
console.log("payload", payload);
|
||||
|
||||
await updateInstanceInfo(payload)
|
||||
.then(() =>
|
||||
@@ -86,7 +74,6 @@ export const GeneralConfigurationForm: FC<IGeneralConfigurationForm> = observer(
|
||||
value={instanceAdmins[0]?.user_detail?.email ?? ""}
|
||||
placeholder="Admin email"
|
||||
className="w-full cursor-not-allowed !text-custom-text-400"
|
||||
autoComplete="on"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
@@ -106,8 +93,7 @@ export const GeneralConfigurationForm: FC<IGeneralConfigurationForm> = observer(
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="text-lg font-medium">Chat + telemetry</div>
|
||||
<IntercomConfig isTelemetryEnabled={watch("is_telemetry_enabled") ?? false} />
|
||||
<div className="text-lg font-medium">Telemetry</div>
|
||||
<div className="flex items-center gap-14 px-4 py-3 border border-custom-border-200 rounded">
|
||||
<div className="grow flex items-center gap-4">
|
||||
<div className="shrink-0">
|
||||
@@ -117,18 +103,17 @@ export const GeneralConfigurationForm: FC<IGeneralConfigurationForm> = observer(
|
||||
</div>
|
||||
<div className="grow">
|
||||
<div className="text-sm font-medium text-custom-text-100 leading-5">
|
||||
Let Plane collect anonymous usage data
|
||||
Allow Plane to collect anonymous usage events
|
||||
</div>
|
||||
<div className="text-xs font-normal text-custom-text-300 leading-5">
|
||||
No PII is collected.This anonymized data is used to understand how you use Plane and build new features
|
||||
in line with{" "}
|
||||
We collect usage events without any PII to analyse and improve Plane.{" "}
|
||||
<a
|
||||
href="https://docs.plane.so/self-hosting/telemetry"
|
||||
target="_blank"
|
||||
className="text-custom-primary-100 hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
our Telemetry Policy.
|
||||
Know more.
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { FC, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
import { MessageSquare } from "lucide-react";
|
||||
import { IFormattedInstanceConfiguration } from "@plane/types";
|
||||
import { ToggleSwitch } from "@plane/ui";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
type TIntercomConfig = {
|
||||
isTelemetryEnabled: boolean;
|
||||
};
|
||||
|
||||
export const IntercomConfig: FC<TIntercomConfig> = observer((props) => {
|
||||
const { isTelemetryEnabled } = props;
|
||||
// hooks
|
||||
const { instanceConfigurations, updateInstanceConfigurations, fetchInstanceConfigurations } = useInstance();
|
||||
// states
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
|
||||
// derived values
|
||||
const isIntercomEnabled = isTelemetryEnabled
|
||||
? instanceConfigurations
|
||||
? instanceConfigurations?.find((config) => config.key === "IS_INTERCOM_ENABLED")?.value === "1"
|
||||
? true
|
||||
: false
|
||||
: undefined
|
||||
: false;
|
||||
|
||||
const { isLoading } = useSWR(isTelemetryEnabled ? "INSTANCE_CONFIGURATIONS" : null, () =>
|
||||
isTelemetryEnabled ? fetchInstanceConfigurations() : null
|
||||
);
|
||||
|
||||
const initialLoader = isLoading && isIntercomEnabled === undefined;
|
||||
|
||||
const submitInstanceConfigurations = async (payload: Partial<IFormattedInstanceConfiguration>) => {
|
||||
try {
|
||||
await updateInstanceConfigurations(payload);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const enableIntercomConfig = () => {
|
||||
submitInstanceConfigurations({ IS_INTERCOM_ENABLED: isIntercomEnabled ? "0" : "1" });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-14 px-4 py-3 border border-custom-border-200 rounded">
|
||||
<div className="grow flex items-center gap-4">
|
||||
<div className="shrink-0">
|
||||
<div className="flex items-center justify-center w-10 h-10 bg-custom-background-80 rounded-full">
|
||||
<MessageSquare className="w-6 h-6 text-custom-text-300/80 p-0.5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grow">
|
||||
<div className="text-sm font-medium text-custom-text-100 leading-5">Chat with us</div>
|
||||
<div className="text-xs font-normal text-custom-text-300 leading-5">
|
||||
Let your users chat with us via Intercom or another service. Toggling Telemetry off turns this off
|
||||
automatically.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto">
|
||||
<ToggleSwitch
|
||||
value={isIntercomEnabled ? true : false}
|
||||
onChange={enableIntercomConfig}
|
||||
size="sm"
|
||||
disabled={!isTelemetryEnabled || isSubmitting || initialLoader}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -7,7 +7,7 @@ import { GeneralConfigurationForm } from "./form";
|
||||
|
||||
function GeneralPage() {
|
||||
const { instance, instanceAdmins } = useInstance();
|
||||
|
||||
console.log("instance", instance);
|
||||
return (
|
||||
<>
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// constants
|
||||
import { ORGANIZATION_SIZE, RESTRICTED_URLS } from "@plane/constants";
|
||||
// types
|
||||
import { IWorkspace } from "@plane/types";
|
||||
// components
|
||||
import { Button, CustomSelect, getButtonStyling, Input, setToast, TOAST_TYPE } from "@plane/ui";
|
||||
// helpers
|
||||
import { WEB_BASE_URL } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
// services
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
export const WorkspaceCreateForm = () => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
// states
|
||||
const [slugError, setSlugError] = useState(false);
|
||||
const [invalidSlug, setInvalidSlug] = useState(false);
|
||||
const [defaultValues, setDefaultValues] = useState<Partial<IWorkspace>>({
|
||||
name: "",
|
||||
slug: "",
|
||||
organization_size: "",
|
||||
});
|
||||
// store hooks
|
||||
const { createWorkspace } = useWorkspace();
|
||||
// form info
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
setValue,
|
||||
getValues,
|
||||
formState: { errors, isSubmitting, isValid },
|
||||
} = useForm<IWorkspace>({ defaultValues, mode: "onChange" });
|
||||
|
||||
const handleCreateWorkspace = async (formData: IWorkspace) => {
|
||||
await workspaceService
|
||||
.workspaceSlugCheck(formData.slug)
|
||||
.then(async (res) => {
|
||||
if (res.status === true && !RESTRICTED_URLS.includes(formData.slug)) {
|
||||
setSlugError(false);
|
||||
await createWorkspace(formData)
|
||||
.then(async () => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Workspace created successfully.",
|
||||
});
|
||||
router.push(`/workspace`);
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Workspace could not be created. Please try again.",
|
||||
});
|
||||
});
|
||||
} else setSlugError(true);
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Some error occurred while creating workspace. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
// when the component unmounts set the default values to whatever user typed in
|
||||
setDefaultValues(getValues());
|
||||
},
|
||||
[getValues, setDefaultValues]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="grid-col grid w-full max-w-4xl grid-cols-1 items-start justify-between gap-x-10 gap-y-6 lg:grid-cols-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-custom-text-300">Name your workspace</h4>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
rules={{
|
||||
required: "This is a required field.",
|
||||
validate: (value) =>
|
||||
/^[\w\s-]*$/.test(value) ||
|
||||
`Workspaces names can contain only (" "), ( - ), ( _ ) and alphanumeric characters.`,
|
||||
maxLength: {
|
||||
value: 80,
|
||||
message: "Limit your name to 80 characters.",
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, ref, onChange } }) => (
|
||||
<Input
|
||||
id="workspaceName"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
setValue("name", e.target.value);
|
||||
setValue("slug", e.target.value.toLocaleLowerCase().trim().replace(/ /g, "-"), {
|
||||
shouldValidate: true,
|
||||
});
|
||||
}}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Something familiar and recognizable is always best."
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<span className="text-xs text-red-500">{errors?.name?.message}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-custom-text-300">Set your workspace's URL</h4>
|
||||
<div className="flex gap-0.5 w-full items-center rounded-md border-[0.5px] border-custom-border-200 px-3">
|
||||
<span className="whitespace-nowrap text-sm text-custom-text-200">{WEB_BASE_URL}/</span>
|
||||
<Controller
|
||||
control={control}
|
||||
name="slug"
|
||||
rules={{
|
||||
required: "The URL is a required field.",
|
||||
maxLength: {
|
||||
value: 48,
|
||||
message: "Limit your URL to 48 characters.",
|
||||
},
|
||||
}}
|
||||
render={({ field: { onChange, value, ref } }) => (
|
||||
<Input
|
||||
id="workspaceUrl"
|
||||
type="text"
|
||||
value={value.toLocaleLowerCase().trim().replace(/ /g, "-")}
|
||||
onChange={(e) => {
|
||||
if (/^[a-zA-Z0-9_-]+$/.test(e.target.value)) setInvalidSlug(false);
|
||||
else setInvalidSlug(true);
|
||||
onChange(e.target.value.toLowerCase());
|
||||
}}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.slug)}
|
||||
placeholder="workspace-name"
|
||||
className="block w-full rounded-md border-none bg-transparent !px-0 py-2 text-sm"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{slugError && <p className="text-sm text-red-500">This URL is taken. Try something else.</p>}
|
||||
{invalidSlug && (
|
||||
<p className="text-sm text-red-500">{`URLs can contain only ( - ), ( _ ) and alphanumeric characters.`}</p>
|
||||
)}
|
||||
{errors.slug && <span className="text-xs text-red-500">{errors.slug.message}</span>}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-custom-text-300">How many people will use this workspace?</h4>
|
||||
<div className="w-full">
|
||||
<Controller
|
||||
name="organization_size"
|
||||
control={control}
|
||||
rules={{ required: "This is a required field." }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
label={
|
||||
ORGANIZATION_SIZE.find((c) => c === value) ?? (
|
||||
<span className="text-custom-text-400">Select a range</span>
|
||||
)
|
||||
}
|
||||
buttonClassName="!border-[0.5px] !border-custom-border-200 !shadow-none"
|
||||
input
|
||||
optionsClassName="w-full"
|
||||
>
|
||||
{ORGANIZATION_SIZE.map((item) => (
|
||||
<CustomSelect.Option key={item} value={item}>
|
||||
{item}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
{errors.organization_size && (
|
||||
<span className="text-sm text-red-500">{errors.organization_size.message}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex max-w-4xl items-center py-1 gap-4">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={handleSubmit(handleCreateWorkspace)}
|
||||
disabled={!isValid}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Creating workspace" : "Create workspace"}
|
||||
</Button>
|
||||
<Link className={getButtonStyling("neutral-primary", "sm")} href="/workspace">
|
||||
Go back
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { WorkspaceCreateForm } from "./form";
|
||||
|
||||
const WorkspaceCreatePage = observer(() => (
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-xl font-medium text-custom-text-100">Create a new workspace on this instance.</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
You will need to invite users from Workspace Settings after you create this workspace.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
||||
<WorkspaceCreateForm />
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
|
||||
export default WorkspaceCreatePage;
|
||||
@@ -1,12 +0,0 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Metadata } from "next";
|
||||
// layouts
|
||||
import { AdminLayout } from "@/layouts/admin-layout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Workspace Management - Plane Web",
|
||||
};
|
||||
|
||||
export default function WorkspaceManagementLayout({ children }: { children: ReactNode }) {
|
||||
return <AdminLayout>{children}</AdminLayout>;
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import useSWR from "swr";
|
||||
import { Loader as LoaderIcon } from "lucide-react";
|
||||
// types
|
||||
import { TInstanceConfigurationKeys } from "@plane/types";
|
||||
// ui
|
||||
import { Button, getButtonStyling, Loader, setPromiseToast, ToggleSwitch } from "@plane/ui";
|
||||
// components
|
||||
import { WorkspaceListItem } from "@/components/workspace";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useInstance, useWorkspace } from "@/hooks/store";
|
||||
|
||||
const WorkspaceManagementPage = observer(() => {
|
||||
// states
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
// store
|
||||
const { formattedConfig, fetchInstanceConfigurations, updateInstanceConfigurations } = useInstance();
|
||||
const {
|
||||
workspaceIds,
|
||||
loader: workspaceLoader,
|
||||
paginationInfo,
|
||||
fetchWorkspaces,
|
||||
fetchNextWorkspaces,
|
||||
} = useWorkspace();
|
||||
// derived values
|
||||
const disableWorkspaceCreation = formattedConfig?.DISABLE_WORKSPACE_CREATION ?? "";
|
||||
const hasNextPage = paginationInfo?.next_page_results && paginationInfo?.next_cursor !== undefined;
|
||||
|
||||
// fetch data
|
||||
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
|
||||
useSWR("INSTANCE_WORKSPACES", () => fetchWorkspaces());
|
||||
|
||||
const updateConfig = async (key: TInstanceConfigurationKeys, value: string) => {
|
||||
setIsSubmitting(true);
|
||||
|
||||
const payload = {
|
||||
[key]: value,
|
||||
};
|
||||
|
||||
const updateConfigPromise = updateInstanceConfigurations(payload);
|
||||
|
||||
setPromiseToast(updateConfigPromise, {
|
||||
loading: "Saving configuration",
|
||||
success: {
|
||||
title: "Success",
|
||||
message: () => "Configuration saved successfully",
|
||||
},
|
||||
error: {
|
||||
title: "Error",
|
||||
message: () => "Failed to save configuration",
|
||||
},
|
||||
});
|
||||
|
||||
await updateConfigPromise
|
||||
.then(() => {
|
||||
setIsSubmitting(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
setIsSubmitting(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="flex items-center justify-between gap-4 border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-xl font-medium text-custom-text-100">Workspaces on this instance</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
See all workspaces and control who can create them.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
||||
<div className="space-y-3">
|
||||
{formattedConfig ? (
|
||||
<div className={cn("w-full flex items-center gap-14 rounded")}>
|
||||
<div className="flex grow items-center gap-4">
|
||||
<div className="grow">
|
||||
<div className="text-lg font-medium pb-1">Prevent anyone else from creating a workspace.</div>
|
||||
<div className={cn("font-normal leading-5 text-custom-text-300 text-xs")}>
|
||||
Toggling this on will let only you create workspaces. You will have to invite users to new
|
||||
workspaces.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`shrink-0 pr-4 ${isSubmitting && "opacity-70"}`}>
|
||||
<div className="flex items-center gap-4">
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(disableWorkspaceCreation))}
|
||||
onChange={() => {
|
||||
if (Boolean(parseInt(disableWorkspaceCreation)) === true) {
|
||||
updateConfig("DISABLE_WORKSPACE_CREATION", "0");
|
||||
} else {
|
||||
updateConfig("DISABLE_WORKSPACE_CREATION", "1");
|
||||
}
|
||||
}}
|
||||
size="sm"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Loader>
|
||||
<Loader.Item height="50px" width="100%" />
|
||||
</Loader>
|
||||
)}
|
||||
{workspaceLoader !== "init-loader" ? (
|
||||
<>
|
||||
<div className="pt-6 flex items-center justify-between gap-2">
|
||||
<div className="flex flex-col items-start gap-x-2">
|
||||
<div className="flex items-center gap-2 text-lg font-medium">
|
||||
All workspaces on this instance{" "}
|
||||
<span className="text-custom-text-300">• {workspaceIds.length}</span>
|
||||
{workspaceLoader && ["mutation", "pagination"].includes(workspaceLoader) && (
|
||||
<LoaderIcon className="w-4 h-4 animate-spin" />
|
||||
)}
|
||||
</div>
|
||||
<div className={cn("font-normal leading-5 text-custom-text-300 text-xs")}>
|
||||
You can't yet delete workspaces and you can only go to the workspace if you are an Admin or a
|
||||
Member.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href="/workspace/create" className={getButtonStyling("primary", "sm")}>
|
||||
Create workspace
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 py-2">
|
||||
{workspaceIds.map((workspaceId) => (
|
||||
<WorkspaceListItem key={workspaceId} workspaceId={workspaceId} />
|
||||
))}
|
||||
</div>
|
||||
{hasNextPage && (
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
variant="link-primary"
|
||||
onClick={() => fetchNextWorkspaces()}
|
||||
disabled={workspaceLoader === "pagination"}
|
||||
>
|
||||
Load more
|
||||
{workspaceLoader === "pagination" && <LoaderIcon className="w-3 h-3 animate-spin" />}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Loader className="space-y-10 py-8">
|
||||
<Loader.Item height="24px" width="20%" />
|
||||
<Loader.Item height="92px" width="100%" />
|
||||
<Loader.Item height="92px" width="100%" />
|
||||
<Loader.Item height="92px" width="100%" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default WorkspaceManagementPage;
|
||||
@@ -10,11 +10,10 @@ import {
|
||||
// components
|
||||
import { AuthenticationMethodCard } from "@/components/authentication";
|
||||
// helpers
|
||||
import { UpgradeButton } from "@/components/common/upgrade-button";
|
||||
import { getBaseAuthenticationModes } from "@/helpers/authentication.helper";
|
||||
// plane admin components
|
||||
import { UpgradeButton } from "@/plane-admin/components/common";
|
||||
// images
|
||||
import OIDCLogo from "@/public/logos/oidc-logo.svg";
|
||||
import OIDCLogo from "@/public/logos/oidc-logo.png";
|
||||
import SAMLLogo from "@/public/logos/saml-logo.svg";
|
||||
|
||||
export type TAuthenticationModeProps = {
|
||||
@@ -28,24 +27,24 @@ export const getAuthenticationModes: (props: TGetBaseAuthenticationModeProps) =>
|
||||
updateConfig,
|
||||
resolvedTheme,
|
||||
}) => [
|
||||
...getBaseAuthenticationModes({ disabled, updateConfig, resolvedTheme }),
|
||||
{
|
||||
key: "oidc",
|
||||
name: "OIDC",
|
||||
description: "Authenticate your users via the OpenID Connect protocol.",
|
||||
icon: <Image src={OIDCLogo} height={22} width={22} alt="OIDC Logo" />,
|
||||
config: <UpgradeButton />,
|
||||
unavailable: true,
|
||||
},
|
||||
{
|
||||
key: "saml",
|
||||
name: "SAML",
|
||||
description: "Authenticate your users via the Security Assertion Markup Language protocol.",
|
||||
icon: <Image src={SAMLLogo} height={22} width={22} alt="SAML Logo" className="pl-0.5" />,
|
||||
config: <UpgradeButton />,
|
||||
unavailable: true,
|
||||
},
|
||||
];
|
||||
...getBaseAuthenticationModes({ disabled, updateConfig, resolvedTheme }),
|
||||
{
|
||||
key: "oidc",
|
||||
name: "OIDC",
|
||||
description: "Authenticate your users via the OpenID Connect protocol.",
|
||||
icon: <Image src={OIDCLogo} height={20} width={20} alt="OIDC Logo" />,
|
||||
config: <UpgradeButton />,
|
||||
unavailable: true,
|
||||
},
|
||||
{
|
||||
key: "saml",
|
||||
name: "SAML",
|
||||
description: "Authenticate your users via the Security Assertion Markup Language protocol.",
|
||||
icon: <Image src={SAMLLogo} height={24} width={24} alt="SAML Logo" className="pb-0.5 pl-0.5" />,
|
||||
config: <UpgradeButton />,
|
||||
unavailable: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const AuthenticationModes: React.FC<TAuthenticationModeProps> = observer((props) => {
|
||||
const { disabled, updateConfig } = props;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./upgrade-button";
|
||||
@@ -1,19 +0,0 @@
|
||||
import { enableStaticRendering } from "mobx-react";
|
||||
// stores
|
||||
import { CoreRootStore } from "@/store/root.store";
|
||||
|
||||
enableStaticRendering(typeof window === "undefined");
|
||||
|
||||
export class RootStore extends CoreRootStore {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
hydrate(initialData: any) {
|
||||
super.hydrate(initialData);
|
||||
}
|
||||
|
||||
resetOnSignOut() {
|
||||
super.resetOnSignOut();
|
||||
}
|
||||
}
|
||||
@@ -52,13 +52,13 @@ export const HelpSection: FC = observer(() => {
|
||||
)}
|
||||
>
|
||||
<div className={`flex items-center gap-1 ${isSidebarCollapsed ? "flex-col justify-center" : "w-full"}`}>
|
||||
<Tooltip tooltipContent="Redirect to Plane" position="right" className="ml-4" disabled={!isSidebarCollapsed}>
|
||||
<Tooltip tooltipContent="Redirect to plane" position="right" className="ml-4" disabled={!isSidebarCollapsed}>
|
||||
<a
|
||||
href={redirectionLink}
|
||||
className={`relative px-2 py-1.5 flex items-center gap-2 font-medium rounded border border-custom-primary-100/20 bg-custom-primary-100/10 text-xs text-custom-primary-200 whitespace-nowrap`}
|
||||
>
|
||||
<ExternalLink size={14} />
|
||||
{!isSidebarCollapsed && "Redirect to Plane"}
|
||||
{!isSidebarCollapsed && "Redirect to plane"}
|
||||
</a>
|
||||
</Tooltip>
|
||||
<Tooltip tooltipContent="Help" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
|
||||
@@ -96,7 +96,7 @@ export const HelpSection: FC = observer(() => {
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
className={`absolute bottom-2 min-w-[10rem] z-[15] ${
|
||||
className={`absolute bottom-2 min-w-[10rem] ${
|
||||
isSidebarCollapsed ? "left-full" : "-left-[75px]"
|
||||
} divide-y divide-custom-border-200 whitespace-nowrap rounded bg-custom-background-100 p-1 shadow-custom-shadow-xs`}
|
||||
ref={helpOptionsRef}
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
import { FC, useEffect, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane helpers
|
||||
import { useOutsideClickDetector } from "@plane/helpers";
|
||||
// components
|
||||
import { HelpSection, SidebarMenu, SidebarDropdown } from "@/components/admin-sidebar";
|
||||
// hooks
|
||||
import { HelpSection, SidebarMenu, SidebarDropdown } from "@/components/admin-sidebar";
|
||||
import { useTheme } from "@/hooks/store";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
// components
|
||||
|
||||
export const InstanceSidebar: FC = observer(() => {
|
||||
export interface IInstanceSidebar {}
|
||||
|
||||
export const InstanceSidebar: FC<IInstanceSidebar> = observer(() => {
|
||||
// store
|
||||
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
||||
|
||||
|
||||
@@ -5,13 +5,11 @@ import { observer } from "mobx-react";
|
||||
import { useTheme as useNextTheme } from "next-themes";
|
||||
import { LogOut, UserCog2, Palette } from "lucide-react";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// plane ui
|
||||
import { Avatar } from "@plane/ui";
|
||||
// helpers
|
||||
import { API_BASE_URL, cn } from "@/helpers/common.helper";
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// hooks
|
||||
import { API_BASE_URL, cn } from "@/helpers/common.helper";
|
||||
import { useTheme, useUser } from "@/hooks/store";
|
||||
// helpers
|
||||
// services
|
||||
import { AuthService } from "@/services/auth.service";
|
||||
|
||||
@@ -124,7 +122,7 @@ export const SidebarDropdown = observer(() => {
|
||||
<Menu.Button className="grid place-items-center outline-none">
|
||||
<Avatar
|
||||
name={currentUser.display_name}
|
||||
src={getFileURL(currentUser.avatar_url)}
|
||||
src={currentUser.avatar ?? undefined}
|
||||
size={24}
|
||||
shape="square"
|
||||
className="!text-base"
|
||||
|
||||
@@ -4,7 +4,7 @@ import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
|
||||
import { Tooltip, WorkspaceIcon } from "@plane/ui";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// hooks
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { useTheme } from "@/hooks/store";
|
||||
@@ -14,37 +14,31 @@ const INSTANCE_ADMIN_LINKS = [
|
||||
{
|
||||
Icon: Cog,
|
||||
name: "General",
|
||||
description: "Identify your instances and get key details.",
|
||||
description: "Identify your instances and get key details",
|
||||
href: `/general/`,
|
||||
},
|
||||
{
|
||||
Icon: WorkspaceIcon,
|
||||
name: "Workspaces",
|
||||
description: "Manage all workspaces on this instance.",
|
||||
href: `/workspace/`,
|
||||
},
|
||||
{
|
||||
Icon: Mail,
|
||||
name: "Email",
|
||||
description: "Configure your SMTP controls.",
|
||||
description: "Set up emails to your users",
|
||||
href: `/email/`,
|
||||
},
|
||||
{
|
||||
Icon: Lock,
|
||||
name: "Authentication",
|
||||
description: "Configure authentication modes.",
|
||||
description: "Configure authentication modes",
|
||||
href: `/authentication/`,
|
||||
},
|
||||
{
|
||||
Icon: BrainCog,
|
||||
name: "Artificial intelligence",
|
||||
description: "Configure your OpenAI creds.",
|
||||
description: "Configure your OpenAI creds",
|
||||
href: `/ai/`,
|
||||
},
|
||||
{
|
||||
Icon: Image,
|
||||
name: "Images in Plane",
|
||||
description: "Allow third-party image libraries.",
|
||||
description: "Allow third-party image libraries",
|
||||
href: `/image/`,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -33,10 +33,6 @@ export const InstanceHeader: FC = observer(() => {
|
||||
return "Github";
|
||||
case "gitlab":
|
||||
return "GitLab";
|
||||
case "workspace":
|
||||
return "Workspace";
|
||||
case "create":
|
||||
return "Create";
|
||||
default:
|
||||
return pathName.toUpperCase();
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { FC } from "react";
|
||||
import { Info, X } from "lucide-react";
|
||||
// helpers
|
||||
import { TAuthErrorInfo } from "@/helpers/authentication.helper";
|
||||
|
||||
type TAuthBanner = {
|
||||
bannerData: TAuthErrorInfo | undefined;
|
||||
handleBannerData?: (bannerData: TAuthErrorInfo | undefined) => void;
|
||||
};
|
||||
|
||||
export const AuthBanner: FC<TAuthBanner> = (props) => {
|
||||
const { bannerData, handleBannerData } = props;
|
||||
|
||||
if (!bannerData) return <></>;
|
||||
return (
|
||||
<div className="relative flex items-center p-2 rounded-md gap-2 border border-custom-primary-100/50 bg-custom-primary-100/10">
|
||||
<div className="w-4 h-4 flex-shrink-0 relative flex justify-center items-center">
|
||||
<Info size={16} className="text-custom-primary-100" />
|
||||
</div>
|
||||
<div className="w-full text-sm font-medium text-custom-primary-100">{bannerData?.message}</div>
|
||||
<div
|
||||
className="relative ml-auto w-6 h-6 rounded-sm flex justify-center items-center transition-all cursor-pointer hover:bg-custom-primary-100/20 text-custom-primary-100/80"
|
||||
onClick={() => handleBannerData && handleBannerData(undefined)}
|
||||
>
|
||||
<X className="w-4 h-4 flex-shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from "./auth-banner";
|
||||
export * from "./email-config-switch";
|
||||
export * from "./password-config-switch";
|
||||
export * from "./authentication-method-card";
|
||||
|
||||
@@ -8,3 +8,4 @@ export * from "./empty-state";
|
||||
export * from "./logo-spinner";
|
||||
export * from "./page-header";
|
||||
export * from "./code-block";
|
||||
export * from "./upgrade-button";
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@ import { getButtonStyling } from "@plane/ui";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
||||
export const UpgradeButton: React.FC = () => (
|
||||
<a href="https://plane.so/pricing?mode=self-hosted" target="_blank" className={cn(getButtonStyling("primary", "sm"))}>
|
||||
Upgrade
|
||||
<a href="https://plane.so/one" target="_blank" className={cn(getButtonStyling("primary", "sm"))}>
|
||||
Available on One
|
||||
<SquareArrowOutUpRight className="h-3.5 w-3.5 p-0.5" />
|
||||
</a>
|
||||
);
|
||||
@@ -7,7 +7,11 @@ import { Button } from "@plane/ui";
|
||||
import InstanceFailureDarkImage from "@/public/instance/instance-failure-dark.svg";
|
||||
import InstanceFailureImage from "@/public/instance/instance-failure.svg";
|
||||
|
||||
export const InstanceFailureView: FC = () => {
|
||||
type InstanceFailureViewProps = {
|
||||
// mutate: () => void;
|
||||
};
|
||||
|
||||
export const InstanceFailureView: FC<InstanceFailureViewProps> = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const instanceImage = resolvedTheme === "dark" ? InstanceFailureDarkImage : InstanceFailureImage;
|
||||
|
||||
@@ -174,7 +174,6 @@ export const InstanceSetupForm: FC = (props) => {
|
||||
placeholder="Wilber"
|
||||
value={formData.first_name}
|
||||
onChange={(e) => handleFormChange("first_name", e.target.value)}
|
||||
autoComplete="on"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
@@ -191,7 +190,6 @@ export const InstanceSetupForm: FC = (props) => {
|
||||
placeholder="Wright"
|
||||
value={formData.last_name}
|
||||
onChange={(e) => handleFormChange("last_name", e.target.value)}
|
||||
autoComplete="on"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,7 +208,6 @@ export const InstanceSetupForm: FC = (props) => {
|
||||
value={formData.email}
|
||||
onChange={(e) => handleFormChange("email", e.target.value)}
|
||||
hasError={errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL ? true : false}
|
||||
autoComplete="on"
|
||||
/>
|
||||
{errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL && errorData.message && (
|
||||
<p className="px-1 text-xs text-red-500">{errorData.message}</p>
|
||||
@@ -250,7 +247,6 @@ export const InstanceSetupForm: FC = (props) => {
|
||||
hasError={errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD ? true : false}
|
||||
onFocus={() => setIsPasswordInputFocused(true)}
|
||||
onBlur={() => setIsPasswordInputFocused(false)}
|
||||
autoComplete="on"
|
||||
/>
|
||||
{showPassword.password ? (
|
||||
<button
|
||||
|
||||
@@ -8,16 +8,8 @@ import { Button, Input, Spinner } from "@plane/ui";
|
||||
// components
|
||||
import { Banner } from "@/components/common";
|
||||
// helpers
|
||||
import {
|
||||
authErrorHandler,
|
||||
EAuthenticationErrorCodes,
|
||||
EErrorAlertType,
|
||||
TAuthErrorInfo,
|
||||
} from "@/helpers/authentication.helper";
|
||||
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
import { AuthService } from "@/services/auth.service";
|
||||
import { AuthBanner } from "../authentication";
|
||||
// ui
|
||||
// icons
|
||||
|
||||
@@ -61,11 +53,12 @@ export const InstanceSignInForm: FC = (props) => {
|
||||
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||
const [formData, setFormData] = useState<TFormData>(defaultFromData);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined);
|
||||
|
||||
const handleFormChange = (key: keyof TFormData, value: string | boolean) =>
|
||||
setFormData((prev) => ({ ...prev, [key]: value }));
|
||||
|
||||
console.log("csrfToken", csrfToken);
|
||||
|
||||
useEffect(() => {
|
||||
if (csrfToken === undefined)
|
||||
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
|
||||
@@ -100,15 +93,6 @@ export const InstanceSignInForm: FC = (props) => {
|
||||
[formData.email, formData.password, isSubmitting]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (errorCode) {
|
||||
const errorDetail = authErrorHandler(errorCode?.toString() as EAuthenticationErrorCodes);
|
||||
if (errorDetail) {
|
||||
setErrorInfo(errorDetail);
|
||||
}
|
||||
}
|
||||
}, [errorCode]);
|
||||
|
||||
return (
|
||||
<div className="flex-grow container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10 lg:pt-28 transition-all">
|
||||
<div className="relative flex flex-col space-y-6">
|
||||
@@ -121,11 +105,7 @@ export const InstanceSignInForm: FC = (props) => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{errorData.type && errorData?.message ? (
|
||||
<Banner type="error" message={errorData?.message} />
|
||||
) : (
|
||||
<>{errorInfo && <AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} />}</>
|
||||
)}
|
||||
{errorData.type && errorData?.message && <Banner type="error" message={errorData?.message} />}
|
||||
|
||||
<form
|
||||
className="space-y-4"
|
||||
@@ -149,7 +129,6 @@ export const InstanceSignInForm: FC = (props) => {
|
||||
placeholder="name@company.com"
|
||||
value={formData.email}
|
||||
onChange={(e) => handleFormChange("email", e.target.value)}
|
||||
autoComplete="on"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
@@ -168,7 +147,6 @@ export const InstanceSignInForm: FC = (props) => {
|
||||
placeholder="Enter your password"
|
||||
value={formData.password}
|
||||
onChange={(e) => handleFormChange("password", e.target.value)}
|
||||
autoComplete="on"
|
||||
/>
|
||||
{showPassword ? (
|
||||
<button
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { resolveGeneralTheme } from "helpers/common.helper";
|
||||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useTheme as nextUseTheme } from "next-themes";
|
||||
// ui
|
||||
import { Button, getButtonStyling } from "@plane/ui";
|
||||
// helpers
|
||||
import { WEB_BASE_URL, resolveGeneralTheme } from "helpers/common.helper";
|
||||
// hooks
|
||||
import { useTheme } from "@/hooks/store";
|
||||
// icons
|
||||
@@ -20,6 +20,8 @@ export const NewUserPopup: React.FC = observer(() => {
|
||||
// theme
|
||||
const { resolvedTheme } = nextUseTheme();
|
||||
|
||||
const redirectionLink = encodeURI(WEB_BASE_URL + "/create-workspace");
|
||||
|
||||
if (!isNewUserPopup) return <></>;
|
||||
return (
|
||||
<div className="absolute bottom-8 right-8 p-6 w-96 border border-custom-border-100 shadow-md rounded-lg bg-custom-background-100">
|
||||
@@ -28,12 +30,12 @@ export const NewUserPopup: React.FC = observer(() => {
|
||||
<div className="text-base font-semibold">Create workspace</div>
|
||||
<div className="py-2 text-sm font-medium text-custom-text-300">
|
||||
Instance setup done! Welcome to Plane instance portal. Start your journey with by creating your first
|
||||
workspace.
|
||||
workspace, you will need to login again.
|
||||
</div>
|
||||
<div className="flex items-center gap-4 pt-2">
|
||||
<Link href="/workspace/create" className={getButtonStyling("primary", "sm")}>
|
||||
<a href={redirectionLink} className={getButtonStyling("primary", "sm")}>
|
||||
Create workspace
|
||||
</Link>
|
||||
</a>
|
||||
<Button variant="neutral-primary" size="sm" onClick={toggleNewUserPopup}>
|
||||
Close
|
||||
</Button>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./list-item";
|
||||
@@ -1,82 +0,0 @@
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
// helpers
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { WEB_BASE_URL } from "@/helpers/common.helper";
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
|
||||
type TWorkspaceListItemProps = {
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const WorkspaceListItem = observer(({ workspaceId }: TWorkspaceListItemProps) => {
|
||||
// store hooks
|
||||
const { getWorkspaceById } = useWorkspace();
|
||||
// derived values
|
||||
const workspace = getWorkspaceById(workspaceId);
|
||||
|
||||
if (!workspace) return null;
|
||||
return (
|
||||
<Link
|
||||
key={workspaceId}
|
||||
href={encodeURI(WEB_BASE_URL + "/" + workspace.slug)}
|
||||
target="_blank"
|
||||
className="group flex items-center justify-between p-4 gap-2.5 truncate border border-custom-border-200/70 hover:border-custom-border-200 hover:bg-custom-background-90 rounded-md"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<span
|
||||
className={`relative flex h-8 w-8 flex-shrink-0 items-center justify-center p-2 mt-1 text-xs uppercase ${
|
||||
!workspace?.logo_url && "rounded bg-custom-primary-500 text-white"
|
||||
}`}
|
||||
>
|
||||
{workspace?.logo_url && workspace.logo_url !== "" ? (
|
||||
<img
|
||||
src={getFileURL(workspace.logo_url)}
|
||||
className="absolute left-0 top-0 h-full w-full rounded object-cover"
|
||||
alt="Workspace Logo"
|
||||
/>
|
||||
) : (
|
||||
(workspace?.name?.[0] ?? "...")
|
||||
)}
|
||||
</span>
|
||||
<div className="flex flex-col items-start gap-1">
|
||||
<div className="flex flex-wrap w-full items-center gap-2.5">
|
||||
<h3 className={`text-base font-medium capitalize`}>{workspace.name}</h3>/
|
||||
<Tooltip tooltipContent="The unique URL of your workspace">
|
||||
<h4 className="text-sm text-custom-text-300">[{workspace.slug}]</h4>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{workspace.owner.email && (
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
<h3 className="text-custom-text-200 font-medium">Owned by:</h3>
|
||||
<h4 className="text-custom-text-300">{workspace.owner.email}</h4>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2.5 text-xs">
|
||||
{workspace.total_projects !== null && (
|
||||
<span className="flex items-center gap-1">
|
||||
<h3 className="text-custom-text-200 font-medium">Total projects:</h3>
|
||||
<h4 className="text-custom-text-300">{workspace.total_projects}</h4>
|
||||
</span>
|
||||
)}
|
||||
{workspace.total_members !== null && (
|
||||
<>
|
||||
•
|
||||
<span className="flex items-center gap-1">
|
||||
<h3 className="text-custom-text-200 font-medium">Total members:</h3>
|
||||
<h4 className="text-custom-text-300">{workspace.total_members}</h4>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<ExternalLink size={14} className="text-custom-text-400 group-hover:text-custom-text-200" />
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from "./use-theme";
|
||||
export * from "./use-instance";
|
||||
export * from "./use-user";
|
||||
export * from "./use-workspace";
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/lib/store-provider";
|
||||
import { IWorkspaceStore } from "@/store/workspace.store";
|
||||
|
||||
export const useWorkspace = (): IWorkspaceStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useWorkspace must be used within StoreProvider");
|
||||
return context.workspace;
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
const useOutsideClickDetector = (ref: React.RefObject<HTMLElement>, callback: () => void) => {
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("mousedown", handleClick);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClick);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export default useOutsideClickDetector;
|
||||
@@ -18,7 +18,6 @@ export const AdminLayout: FC<TAdminLayout> = observer((props) => {
|
||||
const { children } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
// store hooks
|
||||
const { isUserLoggedIn } = useUser();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode, createContext } from "react";
|
||||
// plane admin store
|
||||
import { RootStore } from "@/plane-admin/store/root.store";
|
||||
// store
|
||||
import { RootStore } from "@/store/root.store";
|
||||
|
||||
let rootStore = new RootStore();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// helpers
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// services
|
||||
import { APIService } from "@/services/api.service";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// types
|
||||
import type { IUser } from "@plane/types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
// services
|
||||
import { APIService } from "@/services/api.service";
|
||||
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
// types
|
||||
import type { IWorkspace, TWorkspacePaginationInfo } from "@plane/types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
// services
|
||||
import { APIService } from "@/services/api.service";
|
||||
|
||||
export class WorkspaceService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Fetches all workspaces
|
||||
* @returns Promise<TWorkspacePaginationInfo>
|
||||
*/
|
||||
async getWorkspaces(nextPageCursor?: string): Promise<TWorkspacePaginationInfo> {
|
||||
return this.get<TWorkspacePaginationInfo>("/api/instances/workspaces/", {
|
||||
cursor: nextPageCursor,
|
||||
})
|
||||
.then((response) => response.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Checks if a slug is available
|
||||
* @param slug - string
|
||||
* @returns Promise<any>
|
||||
*/
|
||||
async workspaceSlugCheck(slug: string): Promise<any> {
|
||||
return this.get(`/api/instances/workspace-slug-check/?slug=${slug}`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Creates a new workspace
|
||||
* @param data - IWorkspace
|
||||
* @returns Promise<IWorkspace>
|
||||
*/
|
||||
async createWorkspace(data: IWorkspace): Promise<IWorkspace> {
|
||||
return this.post<IWorkspace, IWorkspace>("/api/instances/workspaces/", data)
|
||||
.then((response) => response.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import { EInstanceStatus, TInstanceStatus } from "@/helpers/instance.helper";
|
||||
// services
|
||||
import { InstanceService } from "@/services/instance.service";
|
||||
// root store
|
||||
import { CoreRootStore } from "@/store/root.store";
|
||||
import { RootStore } from "@/store/root.store";
|
||||
|
||||
export interface IInstanceStore {
|
||||
// issues
|
||||
@@ -46,7 +46,7 @@ export class InstanceStore implements IInstanceStore {
|
||||
// service
|
||||
instanceService;
|
||||
|
||||
constructor(private store: CoreRootStore) {
|
||||
constructor(private store: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observable
|
||||
isLoading: observable.ref,
|
||||
|
||||
@@ -3,28 +3,24 @@ import { enableStaticRendering } from "mobx-react";
|
||||
import { IInstanceStore, InstanceStore } from "./instance.store";
|
||||
import { IThemeStore, ThemeStore } from "./theme.store";
|
||||
import { IUserStore, UserStore } from "./user.store";
|
||||
import { IWorkspaceStore, WorkspaceStore } from "./workspace.store";
|
||||
|
||||
enableStaticRendering(typeof window === "undefined");
|
||||
|
||||
export abstract class CoreRootStore {
|
||||
export class RootStore {
|
||||
theme: IThemeStore;
|
||||
instance: IInstanceStore;
|
||||
user: IUserStore;
|
||||
workspace: IWorkspaceStore;
|
||||
|
||||
constructor() {
|
||||
this.theme = new ThemeStore(this);
|
||||
this.instance = new InstanceStore(this);
|
||||
this.user = new UserStore(this);
|
||||
this.workspace = new WorkspaceStore(this);
|
||||
}
|
||||
|
||||
hydrate(initialData: any) {
|
||||
this.theme.hydrate(initialData.theme);
|
||||
this.instance.hydrate(initialData.instance);
|
||||
this.user.hydrate(initialData.user);
|
||||
this.workspace.hydrate(initialData.workspace);
|
||||
}
|
||||
|
||||
resetOnSignOut() {
|
||||
@@ -32,6 +28,5 @@ export abstract class CoreRootStore {
|
||||
this.instance = new InstanceStore(this);
|
||||
this.user = new UserStore(this);
|
||||
this.theme = new ThemeStore(this);
|
||||
this.workspace = new WorkspaceStore(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { action, observable, makeObservable } from "mobx";
|
||||
// root store
|
||||
import { CoreRootStore } from "@/store/root.store";
|
||||
import { RootStore } from "@/store/root.store";
|
||||
|
||||
type TTheme = "dark" | "light";
|
||||
export interface IThemeStore {
|
||||
@@ -21,7 +21,7 @@ export class ThemeStore implements IThemeStore {
|
||||
isSidebarCollapsed: boolean | undefined = undefined;
|
||||
theme: string | undefined = undefined;
|
||||
|
||||
constructor(private store: CoreRootStore) {
|
||||
constructor(private store: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
isNewUserPopup: observable.ref,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { EUserStatus, TUserStatus } from "@/helpers/user.helper";
|
||||
import { AuthService } from "@/services/auth.service";
|
||||
import { UserService } from "@/services/user.service";
|
||||
// root store
|
||||
import { CoreRootStore } from "@/store/root.store";
|
||||
import { RootStore } from "@/store/root.store";
|
||||
|
||||
export interface IUserStore {
|
||||
// observables
|
||||
@@ -31,7 +31,7 @@ export class UserStore implements IUserStore {
|
||||
userService;
|
||||
authService;
|
||||
|
||||
constructor(private store: CoreRootStore) {
|
||||
constructor(private store: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
isLoading: observable.ref,
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
import set from "lodash/set";
|
||||
import { action, observable, runInAction, makeObservable, computed } from "mobx";
|
||||
import { IWorkspace, TLoader, TPaginationInfo } from "@plane/types";
|
||||
// services
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
// root store
|
||||
import { CoreRootStore } from "@/store/root.store";
|
||||
|
||||
export interface IWorkspaceStore {
|
||||
// observables
|
||||
loader: TLoader;
|
||||
workspaces: Record<string, IWorkspace>;
|
||||
paginationInfo: TPaginationInfo | undefined;
|
||||
// computed
|
||||
workspaceIds: string[];
|
||||
// helper actions
|
||||
hydrate: (data: any) => void;
|
||||
getWorkspaceById: (workspaceId: string) => IWorkspace | undefined;
|
||||
// fetch actions
|
||||
fetchWorkspaces: () => Promise<IWorkspace[]>;
|
||||
fetchNextWorkspaces: () => Promise<IWorkspace[]>;
|
||||
// curd actions
|
||||
createWorkspace: (data: IWorkspace) => Promise<IWorkspace>;
|
||||
}
|
||||
|
||||
export class WorkspaceStore implements IWorkspaceStore {
|
||||
// observables
|
||||
loader: TLoader = "init-loader";
|
||||
workspaces: Record<string, IWorkspace> = {};
|
||||
paginationInfo: TPaginationInfo | undefined = undefined;
|
||||
// services
|
||||
workspaceService;
|
||||
|
||||
constructor(private store: CoreRootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
loader: observable,
|
||||
workspaces: observable,
|
||||
paginationInfo: observable,
|
||||
// computed
|
||||
workspaceIds: computed,
|
||||
// helper actions
|
||||
hydrate: action,
|
||||
getWorkspaceById: action,
|
||||
// fetch actions
|
||||
fetchWorkspaces: action,
|
||||
fetchNextWorkspaces: action,
|
||||
// curd actions
|
||||
createWorkspace: action,
|
||||
});
|
||||
this.workspaceService = new WorkspaceService();
|
||||
}
|
||||
|
||||
// computed
|
||||
get workspaceIds() {
|
||||
return Object.keys(this.workspaces);
|
||||
}
|
||||
|
||||
// helper actions
|
||||
/**
|
||||
* @description Hydrates the workspaces
|
||||
* @param data - any
|
||||
*/
|
||||
hydrate = (data: any) => {
|
||||
if (data) this.workspaces = data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Gets a workspace by id
|
||||
* @param workspaceId - string
|
||||
* @returns IWorkspace | undefined
|
||||
*/
|
||||
getWorkspaceById = (workspaceId: string) => this.workspaces[workspaceId];
|
||||
|
||||
// fetch actions
|
||||
/**
|
||||
* @description Fetches all workspaces
|
||||
* @returns Promise<>
|
||||
*/
|
||||
fetchWorkspaces = async (): Promise<IWorkspace[]> => {
|
||||
try {
|
||||
if (this.workspaceIds.length > 0) {
|
||||
this.loader = "mutation";
|
||||
} else {
|
||||
this.loader = "init-loader";
|
||||
}
|
||||
const paginatedWorkspaceData = await this.workspaceService.getWorkspaces();
|
||||
runInAction(() => {
|
||||
const { results, ...paginationInfo } = paginatedWorkspaceData;
|
||||
results.forEach((workspace: IWorkspace) => {
|
||||
set(this.workspaces, [workspace.id], workspace);
|
||||
});
|
||||
set(this, "paginationInfo", paginationInfo);
|
||||
});
|
||||
return paginatedWorkspaceData.results;
|
||||
} catch (error) {
|
||||
console.error("Error fetching workspaces", error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.loader = "loaded";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Fetches the next page of workspaces
|
||||
* @returns Promise<IWorkspace[]>
|
||||
*/
|
||||
fetchNextWorkspaces = async (): Promise<IWorkspace[]> => {
|
||||
if (!this.paginationInfo || this.paginationInfo.next_page_results === false) return [];
|
||||
try {
|
||||
this.loader = "pagination";
|
||||
const paginatedWorkspaceData = await this.workspaceService.getWorkspaces(this.paginationInfo.next_cursor);
|
||||
runInAction(() => {
|
||||
const { results, ...paginationInfo } = paginatedWorkspaceData;
|
||||
results.forEach((workspace: IWorkspace) => {
|
||||
set(this.workspaces, [workspace.id], workspace);
|
||||
});
|
||||
set(this, "paginationInfo", paginationInfo);
|
||||
});
|
||||
return paginatedWorkspaceData.results;
|
||||
} catch (error) {
|
||||
console.error("Error fetching next workspaces", error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.loader = "loaded";
|
||||
}
|
||||
};
|
||||
|
||||
// curd actions
|
||||
/**
|
||||
* @description Creates a new workspace
|
||||
* @param data - IWorkspace
|
||||
* @returns Promise<IWorkspace>
|
||||
*/
|
||||
createWorkspace = async (data: IWorkspace): Promise<IWorkspace> => {
|
||||
try {
|
||||
this.loader = "mutation";
|
||||
const workspace = await this.workspaceService.createWorkspace(data);
|
||||
runInAction(() => {
|
||||
set(this.workspaces, [workspace.id], workspace);
|
||||
});
|
||||
return workspace;
|
||||
} catch (error) {
|
||||
console.error("Error creating workspace", error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.loader = "loaded";
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "ce/components/common";
|
||||
@@ -1 +0,0 @@
|
||||
export * from "ce/store/root.store";
|
||||
@@ -1,14 +0,0 @@
|
||||
// helpers
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
|
||||
/**
|
||||
* @description combine the file path with the base URL
|
||||
* @param {string} path
|
||||
* @returns {string} final URL with the base URL
|
||||
*/
|
||||
export const getFileURL = (path: string): string | undefined => {
|
||||
if (!path) return undefined;
|
||||
const isValidURL = path.startsWith("http");
|
||||
if (isValidURL) return path;
|
||||
return `${API_BASE_URL}${path}`;
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* @description
|
||||
* This function test whether a URL is valid or not.
|
||||
*
|
||||
* It accepts URLs with or without the protocol.
|
||||
* @param {string} url
|
||||
* @returns {boolean}
|
||||
* @example
|
||||
* checkURLValidity("https://example.com") => true
|
||||
* checkURLValidity("example.com") => true
|
||||
* checkURLValidity("example") => false
|
||||
*/
|
||||
export const checkURLValidity = (url: string): boolean => {
|
||||
if (!url) return false;
|
||||
|
||||
// regex to support complex query parameters and fragments
|
||||
const urlPattern =
|
||||
/^(https?:\/\/)?((([a-z\d-]+\.)*[a-z\d-]+\.[a-z]{2,6})|(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}))(:\d+)?(\/[\w.-]*)*(\?[^#\s]*)?(#[\w-]*)?$/i;
|
||||
|
||||
return urlPattern.test(url);
|
||||
};
|
||||
Vendored
+1
-1
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
|
||||
+11
-13
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "admin",
|
||||
"version": "0.24.0",
|
||||
"version": "0.21.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run develop",
|
||||
@@ -8,44 +8,42 @@
|
||||
"build": "next build",
|
||||
"preview": "next build && next start",
|
||||
"start": "next start",
|
||||
"lint": "eslint . --ext .ts,.tsx",
|
||||
"lint:errors": "eslint . --ext .ts,.tsx --quiet"
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@plane/constants": "*",
|
||||
"@plane/helpers": "*",
|
||||
"@plane/types": "*",
|
||||
"@plane/ui": "*",
|
||||
"@sentry/nextjs": "^8.32.0",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/lodash": "^4.17.0",
|
||||
"autoprefixer": "10.4.14",
|
||||
"axios": "^1.7.4",
|
||||
"axios": "^1.6.7",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.356.0",
|
||||
"mobx": "^6.12.0",
|
||||
"mobx-react": "^9.1.1",
|
||||
"next": "^14.2.12",
|
||||
"next": "^14.2.3",
|
||||
"next-themes": "^0.2.1",
|
||||
"postcss": "^8.4.38",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "7.51.5",
|
||||
"react-hook-form": "^7.51.0",
|
||||
"swr": "^2.2.4",
|
||||
"tailwindcss": "3.3.2",
|
||||
"uuid": "^9.0.1",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "*",
|
||||
"@plane/typescript-config": "*",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "18.16.1",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@types/zxcvbn": "^4.4.4",
|
||||
"eslint-config-custom": "*",
|
||||
"tailwind-config-custom": "*",
|
||||
"typescript": "5.3.3"
|
||||
"tsconfig": "*",
|
||||
"typescript": "^5.4.2"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -1,11 +0,0 @@
|
||||
<svg width="92" height="84" viewBox="0 0 92 84" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3695_11896)">
|
||||
<path d="M83.0398 32.6876C74.2901 27.2397 62.0735 23.8553 48.7013 23.8553C21.7918 23.8553 0 37.3101 0 53.9016C0 69.0898 18.1598 81.554 41.685 83.7001V74.9504C25.8364 72.9693 13.95 64.3022 13.95 53.9016C13.95 42.0977 29.4684 32.44 48.7013 32.44C58.2765 32.44 66.9436 34.8338 73.217 38.7134L64.3022 44.2439H92.1197V27.0746L83.0398 32.6876Z" fill="#CCCCCC"/>
|
||||
<path d="M41.6846 8.99736V74.9504V83.7002L55.6346 74.9504V0L41.6846 8.99736Z" fill="#FF6200"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3695_11896">
|
||||
<rect width="92" height="84" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 702 B |
@@ -1,17 +1,16 @@
|
||||
<svg width="700" height="650" viewBox="0 0 700 650" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3262_5767)">
|
||||
<mask id="mask0_3262_5767" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="700" height="650">
|
||||
<path d="M700 0H0V650H700V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_3262_5767)">
|
||||
<path d="M337.682 0L360.832 20.5C377.982 35.7 395.182 50.85 412.132 66.25C521.982 166 614.982 278.25 684.982 407.45C688.582 414.05 691.832 420.85 695.082 427.6L699.982 437.75L694.582 440.6L690.532 434.85L680.032 419.9L672.682 409.2C621.732 335.25 570.682 261.2 500.582 201.95C479.373 183.995 455.969 168.807 430.932 156.75C380.232 132.5 335.132 142.2 296.432 182C259.632 219.85 240.532 266.85 223.282 314.65C221.032 320.8 218.682 326.9 216.332 333L212.232 343.75L203.632 341C208.632 323.6 213.232 306.1 217.832 288.55C228.332 248.8 238.832 209.05 253.432 170.75C268.932 129.95 288.532 90.6 308.082 51.25C316.532 34.2 324.982 17.15 333.082 0H337.682Z" fill="#C22E33"/>
|
||||
<path d="M372.382 491.1C291.082 529.6 94.3829 569.3 1.08287 559.1C-14.1671 478.8 135.482 102.5 208.982 45.5L204.232 56.4C202.115 61.531 199.813 66.5842 197.332 71.55L194.032 78C156.032 151.1 118.082 224.3 98.6329 304.5C91.6287 332.124 87.8038 360.458 87.2328 388.95C86.7328 455.95 128.432 501.55 198.082 504.4C231.582 505.75 265.432 502.25 299.232 498.7C313.932 497.2 328.582 495.65 343.232 494.5C348.632 494.1 353.932 493.45 360.832 492.55L372.382 491.15V491.1Z" fill="#C22E33"/>
|
||||
<path d="M141.233 639.05C118.983 640.75 96.733 642.45 74.583 644.45C279.433 663.95 476.083 630.6 670.083 562.25C606.833 450.75 521.583 362.7 422.483 286.15C423.783 291.05 426.683 294.6 429.533 298.1L431.933 301.1C440.433 312.4 449.333 323.5 458.283 334.6C478.733 360.05 499.183 385.5 514.583 413.5C553.483 484.5 532.383 545.9 456.183 578.3C406.083 599.65 351.333 614.2 297.183 622.9C245.683 631.1 193.433 635.05 141.233 639.05Z" fill="#C22E33"/>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
|
||||
|
||||
<g fill="#C22E33">
|
||||
|
||||
<path d="M7.754 2l.463.41c.343.304.687.607 1.026.915C11.44 5.32 13.3 7.565 14.7 10.149c.072.132.137.268.202.403l.098.203-.108.057-.081-.115-.21-.299-.147-.214c-1.019-1.479-2.04-2.96-3.442-4.145a6.563 6.563 0 00-1.393-.904c-1.014-.485-1.916-.291-2.69.505-.736.757-1.118 1.697-1.463 2.653-.045.123-.092.245-.139.367l-.082.215-.172-.055c.1-.348.192-.698.284-1.049.21-.795.42-1.59.712-2.356.31-.816.702-1.603 1.093-2.39.169-.341.338-.682.5-1.025h.092z"/>
|
||||
|
||||
<path d="M8.448 11.822c-1.626.77-5.56 1.564-7.426 1.36C.717 11.576 3.71 4.05 5.18 2.91l-.095.218a4.638 4.638 0 01-.138.303l-.066.129c-.76 1.462-1.519 2.926-1.908 4.53a7.482 7.482 0 00-.228 1.689c-.01 1.34.824 2.252 2.217 2.309.67.027 1.347-.043 2.023-.114.294-.03.587-.061.88-.084.108-.008.214-.021.352-.039l.231-.028z"/>
|
||||
|
||||
<path d="M3.825 14.781c-.445.034-.89.068-1.333.108 4.097.39 8.03-.277 11.91-1.644-1.265-2.23-2.97-3.991-4.952-5.522.026.098.084.169.141.239l.048.06c.17.226.348.448.527.67.409.509.818 1.018 1.126 1.578.778 1.42.356 2.648-1.168 3.296-1.002.427-2.097.718-3.18.892-1.03.164-2.075.243-3.119.323z"/>
|
||||
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3262_5767">
|
||||
<rect width="700" height="650" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.3 KiB |
+12
-6
@@ -1,15 +1,21 @@
|
||||
{
|
||||
"extends": "@plane/typescript-config/nextjs.json",
|
||||
"extends": "tsconfig/nextjs.json",
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"],
|
||||
"compilerOptions": {
|
||||
"plugins": [{ "name": "next" }],
|
||||
"baseUrl": ".",
|
||||
"jsx": "preserve",
|
||||
"esModuleInterop": true,
|
||||
"paths": {
|
||||
"@/*": ["core/*"],
|
||||
"@/helpers/*": ["helpers/*"],
|
||||
"@/public/*": ["public/*"],
|
||||
"@/plane-admin/*": ["ce/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "next.config.js", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+1
-11
@@ -15,18 +15,12 @@ POSTGRES_DB="plane"
|
||||
POSTGRES_PORT=5432
|
||||
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
|
||||
|
||||
|
||||
# Redis Settings
|
||||
REDIS_HOST="plane-redis"
|
||||
REDIS_PORT="6379"
|
||||
REDIS_URL="redis://${REDIS_HOST}:6379/"
|
||||
|
||||
# RabbitMQ Settings
|
||||
RABBITMQ_HOST="plane-mq"
|
||||
RABBITMQ_PORT="5672"
|
||||
RABBITMQ_USER="plane"
|
||||
RABBITMQ_PASSWORD="plane"
|
||||
RABBITMQ_VHOST="plane"
|
||||
|
||||
# AWS Settings
|
||||
AWS_REGION=""
|
||||
AWS_ACCESS_KEY_ID="access-key"
|
||||
@@ -56,7 +50,3 @@ GUNICORN_WORKERS=2
|
||||
ADMIN_BASE_URL=
|
||||
SPACE_BASE_URL=
|
||||
APP_BASE_URL=
|
||||
|
||||
|
||||
# Hard delete files after days
|
||||
HARD_DELETE_AFTER_DAYS=60
|
||||
+12
-13
@@ -1,30 +1,29 @@
|
||||
FROM python:3.12.5-alpine AS backend
|
||||
FROM python:3.11.1-alpine3.17 AS backend
|
||||
|
||||
# set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
ENV INSTANCE_CHANGELOG_URL https://api.plane.so/api/public/anchor/8e1c2e4c7bc5493eb7731be3862f6960/pages/
|
||||
|
||||
WORKDIR /code
|
||||
|
||||
RUN apk add --no-cache \
|
||||
"libpq" \
|
||||
"libxslt" \
|
||||
"nodejs-current" \
|
||||
"xmlsec"
|
||||
RUN apk --no-cache add \
|
||||
"libpq~=15" \
|
||||
"libxslt~=1.1" \
|
||||
"nodejs-current~=19" \
|
||||
"xmlsec~=1.2"
|
||||
|
||||
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" \
|
||||
"g++~=12.2" \
|
||||
"gcc~=12.2" \
|
||||
"cargo~=1.64" \
|
||||
"git~=2" \
|
||||
"make~=4.3" \
|
||||
"postgresql13-dev~=13" \
|
||||
"libc-dev" \
|
||||
"linux-headers" \
|
||||
&& \
|
||||
|
||||
+11
-12
@@ -1,25 +1,24 @@
|
||||
FROM python:3.12.5-alpine AS backend
|
||||
FROM python:3.11.1-alpine3.17 AS backend
|
||||
|
||||
# set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
ENV INSTANCE_CHANGELOG_URL https://api.plane.so/api/public/anchor/8e1c2e4c7bc5493eb7731be3862f6960/pages/
|
||||
|
||||
RUN apk --no-cache add \
|
||||
"bash~=5.2" \
|
||||
"libpq" \
|
||||
"libxslt" \
|
||||
"nodejs-current" \
|
||||
"xmlsec" \
|
||||
"libpq~=15" \
|
||||
"libxslt~=1.1" \
|
||||
"nodejs-current~=19" \
|
||||
"xmlsec~=1.2" \
|
||||
"libffi-dev" \
|
||||
"bash~=5.2" \
|
||||
"g++" \
|
||||
"gcc" \
|
||||
"cargo" \
|
||||
"git" \
|
||||
"make" \
|
||||
"postgresql-dev" \
|
||||
"g++~=12.2" \
|
||||
"gcc~=12.2" \
|
||||
"cargo~=1.64" \
|
||||
"git~=2" \
|
||||
"make~=4.3" \
|
||||
"postgresql13-dev~=13" \
|
||||
"libc-dev" \
|
||||
"linux-headers"
|
||||
|
||||
|
||||
@@ -26,7 +26,9 @@ def update_description():
|
||||
updated_issues.append(issue)
|
||||
|
||||
Issue.objects.bulk_update(
|
||||
updated_issues, ["description_html", "description_stripped"], batch_size=100
|
||||
updated_issues,
|
||||
["description_html", "description_stripped"],
|
||||
batch_size=100,
|
||||
)
|
||||
print("Success")
|
||||
except Exception as e:
|
||||
@@ -40,7 +42,9 @@ def update_comments():
|
||||
updated_issue_comments = []
|
||||
|
||||
for issue_comment in issue_comments:
|
||||
issue_comment.comment_html = f"<p>{issue_comment.comment_stripped}</p>"
|
||||
issue_comment.comment_html = (
|
||||
f"<p>{issue_comment.comment_stripped}</p>"
|
||||
)
|
||||
updated_issue_comments.append(issue_comment)
|
||||
|
||||
IssueComment.objects.bulk_update(
|
||||
@@ -99,7 +103,9 @@ def updated_issue_sort_order():
|
||||
issue.sort_order = issue.sequence_id * random.randint(100, 500)
|
||||
updated_issues.append(issue)
|
||||
|
||||
Issue.objects.bulk_update(updated_issues, ["sort_order"], batch_size=100)
|
||||
Issue.objects.bulk_update(
|
||||
updated_issues, ["sort_order"], batch_size=100
|
||||
)
|
||||
print("Success")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
@@ -137,7 +143,9 @@ def update_project_cover_images():
|
||||
project.cover_image = project_cover_images[random.randint(0, 19)]
|
||||
updated_projects.append(project)
|
||||
|
||||
Project.objects.bulk_update(updated_projects, ["cover_image"], batch_size=100)
|
||||
Project.objects.bulk_update(
|
||||
updated_projects, ["cover_image"], batch_size=100
|
||||
)
|
||||
print("Success")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
@@ -186,7 +194,9 @@ def update_label_color():
|
||||
|
||||
def create_slack_integration():
|
||||
try:
|
||||
_ = Integration.objects.create(provider="slack", network=2, title="Slack")
|
||||
_ = Integration.objects.create(
|
||||
provider="slack", network=2, title="Slack"
|
||||
)
|
||||
print("Success")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
@@ -212,12 +222,16 @@ def update_integration_verified():
|
||||
|
||||
def update_start_date():
|
||||
try:
|
||||
issues = Issue.objects.filter(state__group__in=["started", "completed"])
|
||||
issues = Issue.objects.filter(
|
||||
state__group__in=["started", "completed"]
|
||||
)
|
||||
updated_issues = []
|
||||
for issue in issues:
|
||||
issue.start_date = issue.created_at.date()
|
||||
updated_issues.append(issue)
|
||||
Issue.objects.bulk_update(updated_issues, ["start_date"], batch_size=500)
|
||||
Issue.objects.bulk_update(
|
||||
updated_issues, ["start_date"], batch_size=500
|
||||
)
|
||||
print("Success")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
@@ -32,3 +32,4 @@ python manage.py create_bucket
|
||||
python manage.py clear_cache
|
||||
|
||||
python manage.py runserver 0.0.0.0:8000 --settings=plane.settings.local
|
||||
|
||||
|
||||
+3
-1
@@ -3,7 +3,9 @@ import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
|
||||
os.environ.setdefault(
|
||||
"DJANGO_SETTINGS_MODULE", "plane.settings.production"
|
||||
)
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"name": "plane-api",
|
||||
"version": "0.24.0"
|
||||
"version": "0.21.0"
|
||||
}
|
||||
|
||||
@@ -25,7 +25,10 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
|
||||
def validate_api_token(self, token):
|
||||
try:
|
||||
api_token = APIToken.objects.get(
|
||||
Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)),
|
||||
Q(
|
||||
Q(expired_at__gt=timezone.now())
|
||||
| Q(expired_at__isnull=True)
|
||||
),
|
||||
token=token,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
@@ -40,44 +40,3 @@ class ApiKeyRateThrottle(SimpleRateThrottle):
|
||||
request.META["X-RateLimit-Reset"] = reset_time
|
||||
|
||||
return allowed
|
||||
|
||||
|
||||
class ServiceTokenRateThrottle(SimpleRateThrottle):
|
||||
scope = "service_token"
|
||||
rate = "300/minute"
|
||||
|
||||
def get_cache_key(self, request, view):
|
||||
# Retrieve the API key from the request header
|
||||
api_key = request.headers.get("X-Api-Key")
|
||||
if not api_key:
|
||||
return None # Allow the request if there's no API key
|
||||
|
||||
# Use the API key as part of the cache key
|
||||
return f"{self.scope}:{api_key}"
|
||||
|
||||
def allow_request(self, request, view):
|
||||
allowed = super().allow_request(request, view)
|
||||
|
||||
if allowed:
|
||||
now = self.timer()
|
||||
# Calculate the remaining limit and reset time
|
||||
history = self.cache.get(self.key, [])
|
||||
|
||||
# Remove old histories
|
||||
while history and history[-1] <= now - self.duration:
|
||||
history.pop()
|
||||
|
||||
# Calculate the requests
|
||||
num_requests = len(history)
|
||||
|
||||
# Check available requests
|
||||
available = self.num_requests - num_requests
|
||||
|
||||
# Unix timestamp for when the rate limit will reset
|
||||
reset_time = int(now + self.duration)
|
||||
|
||||
# Add headers
|
||||
request.META["X-RateLimit-Remaining"] = max(0, available)
|
||||
request.META["X-RateLimit-Reset"] = reset_time
|
||||
|
||||
return allowed
|
||||
|
||||
@@ -5,13 +5,17 @@ from .issue import (
|
||||
IssueSerializer,
|
||||
LabelSerializer,
|
||||
IssueLinkSerializer,
|
||||
IssueAttachmentSerializer,
|
||||
IssueCommentSerializer,
|
||||
IssueAttachmentSerializer,
|
||||
IssueActivitySerializer,
|
||||
IssueExpandSerializer,
|
||||
IssueLiteSerializer,
|
||||
)
|
||||
from .state import StateLiteSerializer, StateSerializer
|
||||
from .cycle import CycleSerializer, CycleIssueSerializer, CycleLiteSerializer
|
||||
from .module import ModuleSerializer, ModuleIssueSerializer, ModuleLiteSerializer
|
||||
from .intake import IntakeIssueSerializer
|
||||
from .module import (
|
||||
ModuleSerializer,
|
||||
ModuleIssueSerializer,
|
||||
ModuleLiteSerializer,
|
||||
)
|
||||
from .inbox import InboxIssueSerializer
|
||||
|
||||
@@ -67,7 +67,6 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
# Import all the expandable serializers
|
||||
from . import (
|
||||
IssueSerializer,
|
||||
IssueLiteSerializer,
|
||||
ProjectLiteSerializer,
|
||||
StateLiteSerializer,
|
||||
UserLiteSerializer,
|
||||
@@ -87,7 +86,6 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
"actor": UserLiteSerializer,
|
||||
"owned_by": UserLiteSerializer,
|
||||
"members": UserLiteSerializer,
|
||||
"parent": IssueLiteSerializer,
|
||||
}
|
||||
# Check if field in expansion then expand the field
|
||||
if expand in expansion:
|
||||
@@ -102,6 +100,8 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
response[expand] = exp_serializer.data
|
||||
else:
|
||||
# You might need to handle this case differently
|
||||
response[expand] = getattr(instance, f"{expand}_id", None)
|
||||
response[expand] = getattr(
|
||||
instance, f"{expand}_id", None
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
@@ -13,9 +13,9 @@ class CycleSerializer(BaseSerializer):
|
||||
started_issues = serializers.IntegerField(read_only=True)
|
||||
unstarted_issues = serializers.IntegerField(read_only=True)
|
||||
backlog_issues = serializers.IntegerField(read_only=True)
|
||||
total_estimates = serializers.FloatField(read_only=True)
|
||||
completed_estimates = serializers.FloatField(read_only=True)
|
||||
started_estimates = serializers.FloatField(read_only=True)
|
||||
total_estimates = serializers.IntegerField(read_only=True)
|
||||
completed_estimates = serializers.IntegerField(read_only=True)
|
||||
started_estimates = serializers.IntegerField(read_only=True)
|
||||
|
||||
def validate(self, data):
|
||||
if (
|
||||
@@ -23,7 +23,9 @@ class CycleSerializer(BaseSerializer):
|
||||
and data.get("end_date", None) is not None
|
||||
and data.get("start_date", None) > data.get("end_date", None)
|
||||
):
|
||||
raise serializers.ValidationError("Start date cannot exceed end date")
|
||||
raise serializers.ValidationError(
|
||||
"Start date cannot exceed end date"
|
||||
)
|
||||
return data
|
||||
|
||||
class Meta:
|
||||
@@ -38,7 +40,6 @@ class CycleSerializer(BaseSerializer):
|
||||
"workspace",
|
||||
"project",
|
||||
"owned_by",
|
||||
"deleted_at",
|
||||
]
|
||||
|
||||
|
||||
@@ -48,7 +49,11 @@ class CycleIssueSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = CycleIssue
|
||||
fields = "__all__"
|
||||
read_only_fields = ["workspace", "project", "cycle"]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"cycle",
|
||||
]
|
||||
|
||||
|
||||
class CycleLiteSerializer(BaseSerializer):
|
||||
|
||||
+4
-5
@@ -1,16 +1,15 @@
|
||||
# Module improts
|
||||
from .base import BaseSerializer
|
||||
from .issue import IssueExpandSerializer
|
||||
from plane.db.models import IntakeIssue
|
||||
from rest_framework import serializers
|
||||
from plane.db.models import InboxIssue
|
||||
|
||||
|
||||
class IntakeIssueSerializer(BaseSerializer):
|
||||
class InboxIssueSerializer(BaseSerializer):
|
||||
|
||||
issue_detail = IssueExpandSerializer(read_only=True, source="issue")
|
||||
inbox = serializers.UUIDField(source="intake.id", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = IntakeIssue
|
||||
model = InboxIssue
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
@@ -1,3 +1,6 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import URLValidator
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from lxml import html
|
||||
@@ -8,10 +11,9 @@ from rest_framework import serializers
|
||||
# Module imports
|
||||
from plane.db.models import (
|
||||
Issue,
|
||||
IssueType,
|
||||
IssueActivity,
|
||||
IssueAssignee,
|
||||
FileAsset,
|
||||
IssueAttachment,
|
||||
IssueComment,
|
||||
IssueLabel,
|
||||
IssueLink,
|
||||
@@ -27,10 +29,6 @@ from .module import ModuleLiteSerializer, ModuleSerializer
|
||||
from .state import StateLiteSerializer
|
||||
from .user import UserLiteSerializer
|
||||
|
||||
# Django imports
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import URLValidator
|
||||
|
||||
|
||||
class IssueSerializer(BaseSerializer):
|
||||
assignees = serializers.ListField(
|
||||
@@ -48,14 +46,22 @@ class IssueSerializer(BaseSerializer):
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
type_id = serializers.PrimaryKeyRelatedField(
|
||||
source="type", queryset=IssueType.objects.all(), required=False, allow_null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
read_only_fields = ["id", "workspace", "project", "updated_by", "updated_at"]
|
||||
exclude = ["description", "description_stripped"]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
exclude = [
|
||||
"description",
|
||||
"description_stripped",
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
if (
|
||||
@@ -63,7 +69,9 @@ class IssueSerializer(BaseSerializer):
|
||||
and data.get("target_date", None) is not None
|
||||
and data.get("start_date", None) > data.get("target_date", None)
|
||||
):
|
||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
||||
raise serializers.ValidationError(
|
||||
"Start date cannot exceed target date"
|
||||
)
|
||||
|
||||
try:
|
||||
if data.get("description_html", None) is not None:
|
||||
@@ -85,14 +93,16 @@ class IssueSerializer(BaseSerializer):
|
||||
# Validate labels are from project
|
||||
if data.get("labels", []):
|
||||
data["labels"] = Label.objects.filter(
|
||||
project_id=self.context.get("project_id"), id__in=data["labels"]
|
||||
project_id=self.context.get("project_id"),
|
||||
id__in=data["labels"],
|
||||
).values_list("id", flat=True)
|
||||
|
||||
# Check state is from the project only else raise validation error
|
||||
if (
|
||||
data.get("state")
|
||||
and not State.objects.filter(
|
||||
project_id=self.context.get("project_id"), pk=data.get("state").id
|
||||
project_id=self.context.get("project_id"),
|
||||
pk=data.get("state").id,
|
||||
).exists()
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
@@ -103,7 +113,8 @@ class IssueSerializer(BaseSerializer):
|
||||
if (
|
||||
data.get("parent")
|
||||
and not Issue.objects.filter(
|
||||
workspace_id=self.context.get("workspace_id"), pk=data.get("parent").id
|
||||
workspace_id=self.context.get("workspace_id"),
|
||||
pk=data.get("parent").id,
|
||||
).exists()
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
@@ -120,18 +131,7 @@ class IssueSerializer(BaseSerializer):
|
||||
workspace_id = self.context["workspace_id"]
|
||||
default_assignee_id = self.context["default_assignee_id"]
|
||||
|
||||
issue_type = validated_data.pop("type", None)
|
||||
|
||||
if not issue_type:
|
||||
# Get default issue type
|
||||
issue_type = IssueType.objects.filter(
|
||||
project_issue_types__project_id=project_id, is_default=True
|
||||
).first()
|
||||
issue_type = issue_type
|
||||
|
||||
issue = Issue.objects.create(
|
||||
**validated_data, project_id=project_id, type=issue_type
|
||||
)
|
||||
issue = Issue.objects.create(**validated_data, project_id=project_id)
|
||||
|
||||
# Issue Audit Users
|
||||
created_by_id = issue.created_by_id
|
||||
@@ -245,20 +245,17 @@ class IssueSerializer(BaseSerializer):
|
||||
]
|
||||
if "labels" in self.fields:
|
||||
if "labels" in self.expand:
|
||||
data["labels"] = LabelSerializer(instance.labels.all(), many=True).data
|
||||
data["labels"] = LabelSerializer(
|
||||
instance.labels.all(), many=True
|
||||
).data
|
||||
else:
|
||||
data["labels"] = [str(label.id) for label in instance.labels.all()]
|
||||
data["labels"] = [
|
||||
str(label.id) for label in instance.labels.all()
|
||||
]
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class IssueLiteSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = Issue
|
||||
fields = ["id", "sequence_id", "project_id"]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class LabelSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = Label
|
||||
@@ -271,7 +268,6 @@ class LabelSerializer(BaseSerializer):
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"deleted_at",
|
||||
]
|
||||
|
||||
|
||||
@@ -307,7 +303,8 @@ class IssueLinkSerializer(BaseSerializer):
|
||||
# Validation if url already exists
|
||||
def create(self, validated_data):
|
||||
if IssueLink.objects.filter(
|
||||
url=validated_data.get("url"), issue_id=validated_data.get("issue_id")
|
||||
url=validated_data.get("url"),
|
||||
issue_id=validated_data.get("issue_id"),
|
||||
).exists():
|
||||
raise serializers.ValidationError(
|
||||
{"error": "URL already exists for this Issue"}
|
||||
@@ -315,13 +312,10 @@ class IssueLinkSerializer(BaseSerializer):
|
||||
return IssueLink.objects.create(**validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
if (
|
||||
IssueLink.objects.filter(
|
||||
url=validated_data.get("url"), issue_id=instance.issue_id
|
||||
)
|
||||
.exclude(pk=instance.id)
|
||||
.exists()
|
||||
):
|
||||
if IssueLink.objects.filter(
|
||||
url=validated_data.get("url"),
|
||||
issue_id=instance.issue_id,
|
||||
).exclude(pk=instance.id).exists():
|
||||
raise serializers.ValidationError(
|
||||
{"error": "URL already exists for this Issue"}
|
||||
)
|
||||
@@ -331,14 +325,16 @@ class IssueLinkSerializer(BaseSerializer):
|
||||
|
||||
class IssueAttachmentSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = FileAsset
|
||||
model = IssueAttachment
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"workspace",
|
||||
"project",
|
||||
"issue",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
@@ -358,7 +354,10 @@ class IssueCommentSerializer(BaseSerializer):
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
exclude = ["comment_stripped", "comment_json"]
|
||||
exclude = [
|
||||
"comment_stripped",
|
||||
"comment_json",
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
try:
|
||||
@@ -375,27 +374,38 @@ class IssueCommentSerializer(BaseSerializer):
|
||||
class IssueActivitySerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = IssueActivity
|
||||
exclude = ["created_by", "updated_by"]
|
||||
exclude = [
|
||||
"created_by",
|
||||
"updated_by",
|
||||
]
|
||||
|
||||
|
||||
class CycleIssueSerializer(BaseSerializer):
|
||||
cycle = CycleSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
fields = ["cycle"]
|
||||
fields = [
|
||||
"cycle",
|
||||
]
|
||||
|
||||
|
||||
class ModuleIssueSerializer(BaseSerializer):
|
||||
module = ModuleSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
fields = ["module"]
|
||||
fields = [
|
||||
"module",
|
||||
]
|
||||
|
||||
|
||||
class LabelLiteSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = Label
|
||||
fields = ["id", "name", "color"]
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"color",
|
||||
]
|
||||
|
||||
|
||||
class IssueExpandSerializer(BaseSerializer):
|
||||
|
||||
@@ -39,7 +39,6 @@ class ModuleSerializer(BaseSerializer):
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"deleted_at",
|
||||
]
|
||||
|
||||
def to_representation(self, instance):
|
||||
@@ -53,11 +52,14 @@ class ModuleSerializer(BaseSerializer):
|
||||
and data.get("target_date", None) is not None
|
||||
and data.get("start_date", None) > data.get("target_date", None)
|
||||
):
|
||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
||||
raise serializers.ValidationError(
|
||||
"Start date cannot exceed target date"
|
||||
)
|
||||
|
||||
if data.get("members", []):
|
||||
data["members"] = ProjectMember.objects.filter(
|
||||
project_id=self.context.get("project_id"), member_id__in=data["members"]
|
||||
project_id=self.context.get("project_id"),
|
||||
member_id__in=data["members"],
|
||||
).values_list("member_id", flat=True)
|
||||
|
||||
return data
|
||||
@@ -68,14 +70,6 @@ class ModuleSerializer(BaseSerializer):
|
||||
project_id = self.context["project_id"]
|
||||
workspace_id = self.context["workspace_id"]
|
||||
|
||||
module_name = validated_data.get("name")
|
||||
if module_name:
|
||||
# Lookup for the module name in the module table for that project
|
||||
if Module.objects.filter(name=module_name, project_id=project_id).exists():
|
||||
raise serializers.ValidationError(
|
||||
{"error": "Module with this name already exists"}
|
||||
)
|
||||
|
||||
module = Module.objects.create(**validated_data, project_id=project_id)
|
||||
if members is not None:
|
||||
ModuleMember.objects.bulk_create(
|
||||
@@ -98,17 +92,6 @@ class ModuleSerializer(BaseSerializer):
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
members = validated_data.pop("members", None)
|
||||
module_name = validated_data.get("name")
|
||||
if module_name:
|
||||
# Lookup for the module name in the module table for that project
|
||||
if (
|
||||
Module.objects.filter(name=module_name, project=instance.project)
|
||||
.exclude(id=instance.id)
|
||||
.exists()
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
{"error": "Module with this name already exists"}
|
||||
)
|
||||
|
||||
if members is not None:
|
||||
ModuleMember.objects.filter(module=instance).delete()
|
||||
@@ -165,7 +148,8 @@ class ModuleLinkSerializer(BaseSerializer):
|
||||
# Validation if url already exists
|
||||
def create(self, validated_data):
|
||||
if ModuleLink.objects.filter(
|
||||
url=validated_data.get("url"), module_id=validated_data.get("module_id")
|
||||
url=validated_data.get("url"),
|
||||
module_id=validated_data.get("module_id"),
|
||||
).exists():
|
||||
raise serializers.ValidationError(
|
||||
{"error": "URL already exists for this Issue"}
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Project, ProjectIdentifier, WorkspaceMember
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
ProjectIdentifier,
|
||||
WorkspaceMember,
|
||||
)
|
||||
|
||||
from .base import BaseSerializer
|
||||
|
||||
@@ -15,8 +19,6 @@ class ProjectSerializer(BaseSerializer):
|
||||
sort_order = serializers.FloatField(read_only=True)
|
||||
member_role = serializers.IntegerField(read_only=True)
|
||||
is_deployed = serializers.BooleanField(read_only=True)
|
||||
cover_image_url = serializers.CharField(read_only=True)
|
||||
inbox_view = serializers.BooleanField(read_only=True, source="intake_view")
|
||||
|
||||
class Meta:
|
||||
model = Project
|
||||
@@ -29,8 +31,6 @@ class ProjectSerializer(BaseSerializer):
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"deleted_at",
|
||||
"cover_image_url",
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
@@ -63,12 +63,16 @@ class ProjectSerializer(BaseSerializer):
|
||||
def create(self, validated_data):
|
||||
identifier = validated_data.get("identifier", "").strip().upper()
|
||||
if identifier == "":
|
||||
raise serializers.ValidationError(detail="Project Identifier is required")
|
||||
raise serializers.ValidationError(
|
||||
detail="Project Identifier is required"
|
||||
)
|
||||
|
||||
if ProjectIdentifier.objects.filter(
|
||||
name=identifier, workspace_id=self.context["workspace_id"]
|
||||
).exists():
|
||||
raise serializers.ValidationError(detail="Project Identifier is taken")
|
||||
raise serializers.ValidationError(
|
||||
detail="Project Identifier is taken"
|
||||
)
|
||||
|
||||
project = Project.objects.create(
|
||||
**validated_data, workspace_id=self.context["workspace_id"]
|
||||
@@ -82,8 +86,6 @@ class ProjectSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class ProjectLiteSerializer(BaseSerializer):
|
||||
cover_image_url = serializers.CharField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Project
|
||||
fields = [
|
||||
@@ -94,6 +96,5 @@ class ProjectLiteSerializer(BaseSerializer):
|
||||
"icon_prop",
|
||||
"emoji",
|
||||
"description",
|
||||
"cover_image_url",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
@@ -7,9 +7,9 @@ class StateSerializer(BaseSerializer):
|
||||
def validate(self, data):
|
||||
# If the default is being provided then make all other states default False
|
||||
if data.get("default", False):
|
||||
State.objects.filter(project_id=self.context.get("project_id")).update(
|
||||
default=False
|
||||
)
|
||||
State.objects.filter(
|
||||
project_id=self.context.get("project_id")
|
||||
).update(default=False)
|
||||
return data
|
||||
|
||||
class Meta:
|
||||
@@ -23,12 +23,16 @@ class StateSerializer(BaseSerializer):
|
||||
"updated_at",
|
||||
"workspace",
|
||||
"project",
|
||||
"deleted_at",
|
||||
]
|
||||
|
||||
|
||||
class StateLiteSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = State
|
||||
fields = ["id", "name", "color", "group"]
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"color",
|
||||
"group",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
@@ -13,7 +13,6 @@ class UserLiteSerializer(BaseSerializer):
|
||||
"last_name",
|
||||
"email",
|
||||
"avatar",
|
||||
"avatar_url",
|
||||
"display_name",
|
||||
"email",
|
||||
]
|
||||
|
||||
@@ -8,5 +8,9 @@ class WorkspaceLiteSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Workspace
|
||||
fields = ["name", "slug", "id"]
|
||||
fields = [
|
||||
"name",
|
||||
"slug",
|
||||
"id",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
@@ -3,8 +3,7 @@ from .state import urlpatterns as state_patterns
|
||||
from .issue import urlpatterns as issue_patterns
|
||||
from .cycle import urlpatterns as cycle_patterns
|
||||
from .module import urlpatterns as module_patterns
|
||||
from .intake import urlpatterns as intake_patterns
|
||||
from .member import urlpatterns as member_patterns
|
||||
from .inbox import urlpatterns as inbox_patterns
|
||||
|
||||
urlpatterns = [
|
||||
*project_patterns,
|
||||
@@ -12,6 +11,5 @@ urlpatterns = [
|
||||
*issue_patterns,
|
||||
*cycle_patterns,
|
||||
*module_patterns,
|
||||
*intake_patterns,
|
||||
*member_patterns,
|
||||
*inbox_patterns,
|
||||
]
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
from django.urls import path
|
||||
|
||||
from plane.api.views import InboxIssueAPIEndpoint
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
|
||||
InboxIssueAPIEndpoint.as_view(),
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:issue_id>/",
|
||||
InboxIssueAPIEndpoint.as_view(),
|
||||
name="inbox-issue",
|
||||
),
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
from plane.api.views import IntakeIssueAPIEndpoint
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
|
||||
IntakeIssueAPIEndpoint.as_view(),
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:issue_id>/",
|
||||
IntakeIssueAPIEndpoint.as_view(),
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/",
|
||||
IntakeIssueAPIEndpoint.as_view(),
|
||||
name="intake-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/<uuid:issue_id>/",
|
||||
IntakeIssueAPIEndpoint.as_view(),
|
||||
name="intake-issue",
|
||||
),
|
||||
]
|
||||
@@ -7,7 +7,6 @@ from plane.api.views import (
|
||||
IssueCommentAPIEndpoint,
|
||||
IssueActivityAPIEndpoint,
|
||||
WorkspaceIssueAPIEndpoint,
|
||||
IssueAttachmentEndpoint,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
@@ -66,9 +65,4 @@ urlpatterns = [
|
||||
IssueActivityAPIEndpoint.as_view(),
|
||||
name="activity",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/",
|
||||
IssueAttachmentEndpoint.as_view(),
|
||||
name="attachment",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
from plane.api.views import ProjectMemberAPIEndpoint
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<str:project_id>/members/",
|
||||
ProjectMemberAPIEndpoint.as_view(),
|
||||
name="users",
|
||||
)
|
||||
]
|
||||
@@ -1,10 +1,15 @@
|
||||
from django.urls import path
|
||||
|
||||
from plane.api.views import ProjectAPIEndpoint, ProjectArchiveUnarchiveAPIEndpoint
|
||||
from plane.api.views import (
|
||||
ProjectAPIEndpoint,
|
||||
ProjectArchiveUnarchiveAPIEndpoint,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/", ProjectAPIEndpoint.as_view(), name="project"
|
||||
"workspaces/<str:slug>/projects/",
|
||||
ProjectAPIEndpoint.as_view(),
|
||||
name="project",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:pk>/",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user