Compare commits

..
Author SHA1 Message Date
sriram veeraghantaandGitHub 1dcea9bcc8 Merge pull request #4569 from makeplane/preview
release: v0.20-dev
2024-05-23 19:55:06 +05:30
sriram veeraghantaandGitHub da957e06b6 Merge pull request #4349 from makeplane/preview
release: v0.19-dev
2024-05-03 20:36:07 +05:30
sriram veeraghantaandGitHub a0b9596cb4 Merge pull request #4239 from makeplane/preview
chore:version update
2024-04-19 12:01:15 +05:30
sriram veeraghantaandGitHub f71e8a3a0f Merge pull request #4238 from makeplane/preview
release: v0.18-dev
2024-04-19 11:56:03 +05:30
sriram veeraghantaandGitHub 002fb4547b Merge pull request #4107 from makeplane/preview
release: v0.17-dev
2024-04-02 20:07:48 +05:30
sriram veeraghantaandGitHub c1b1ba35c1 Merge pull request #3878 from makeplane/preview
release: v0.16-dev
2024-03-05 20:04:08 +05:30
sriram veeraghantaandGitHub 4566d6e80c Merge pull request #3697 from makeplane/preview
release: 0.15.4-dev
2024-02-19 19:30:06 +05:30
sriram veeraghantaandGitHub e8d359e625 Merge pull request #3674 from makeplane/preview
fix: build branch docker images push on release
2024-02-15 14:35:32 +05:30
sriram veeraghantaandGitHub 351eba8d61 Merge pull request #3671 from makeplane/preview
release: peek overview issue description initial load bug (#3670)
2024-02-15 03:25:30 +05:30
sriram veeraghantaandGitHub 1e27e37b51 Merge pull request #3666 from makeplane/preview
release: v0.15.2-dev
2024-02-14 19:41:55 +05:30
sriram veeraghantaandGitHub 7df2e9cf11 Merge pull request #3632 from makeplane/preview
release: v0.15.1-dev
2024-02-12 20:59:56 +05:30
sriram veeraghantaandGitHub c6e3f1b932 Merge pull request #3535 from makeplane/preview
release: 0.15-dev
2024-02-01 15:01:49 +05:30
2035 changed files with 26011 additions and 41345 deletions
-91
View File
@@ -1,91 +0,0 @@
name: Build AIO Base Image
on:
workflow_dispatch:
env:
TARGET_BRANCH: ${{ github.ref_name }}
jobs:
base_build_setup:
name: Build Preparation
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 }}
gh_buildx_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }}
gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }}
gh_buildx_endpoint: ${{ steps.set_env_variables.outputs.BUILDX_ENDPOINT }}
build_base: ${{ steps.changed_files.outputs.base_any_changed }}
steps:
- id: set_env_variables
name: Set Environment Variables
run: |
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
- id: checkout_files
name: Checkout Files
uses: actions/checkout@v4
- name: Get changed files
id: changed_files
uses: tj-actions/changed-files@v42
with:
files_yaml: |
base:
- aio/Dockerfile.base
base_build_push:
if: ${{ needs.base_build_setup.outputs.build_base == 'true' || github.event_name == 'workflow_dispatch' || needs.base_build_setup.outputs.gh_branch_name == 'master' }}
runs-on: ubuntu-latest
needs: [base_build_setup]
env:
BASE_IMG_TAG: makeplane/plane-aio-base:${{ needs.base_build_setup.outputs.gh_branch_name }}
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 }}
BUILDX_ENDPOINT: ${{ needs.base_build_setup.outputs.gh_buildx_endpoint }}
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Set Docker Tag
run: |
if [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=makeplane/plane-aio-base:latest
else
TAG=${{ env.BASE_IMG_TAG }}
fi
echo "BASE_IMG_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: Build and Push to Docker Hub
uses: docker/build-push-action@v5.1.0
with:
context: ./aio
file: ./aio/Dockerfile.base
platforms: ${{ env.BUILDX_PLATFORMS }}
tags: ${{ env.BASE_IMG_TAG }}
push: true
env:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
+16 -16
View File
@@ -14,7 +14,7 @@ env:
jobs:
branch_build_setup:
name: Build Setup
name: Build-Push Web/Space/API/Proxy Docker Image
runs-on: ubuntu-latest
outputs:
gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }}
@@ -85,7 +85,7 @@ jobs:
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
FRONTEND_TAG: makeplane/plane-frontend:${{ needs.branch_build_setup.outputs.gh_branch_name }}
FRONTEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/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 }}
@@ -95,9 +95,9 @@ jobs:
- 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 }}
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:stable,${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=makeplane/plane-frontend:latest
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend:latest
else
TAG=${{ env.FRONTEND_TAG }}
fi
@@ -137,7 +137,7 @@ jobs:
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
ADMIN_TAG: makeplane/plane-admin:${{ needs.branch_build_setup.outputs.gh_branch_name }}
ADMIN_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/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 }}
@@ -147,9 +147,9 @@ jobs:
- 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 }}
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-admin:stable,${{ secrets.DOCKERHUB_USERNAME }}/plane-admin:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=makeplane/plane-admin:latest
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-admin:latest
else
TAG=${{ env.ADMIN_TAG }}
fi
@@ -189,7 +189,7 @@ jobs:
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
SPACE_TAG: makeplane/plane-space:${{ needs.branch_build_setup.outputs.gh_branch_name }}
SPACE_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/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 }}
@@ -199,9 +199,9 @@ jobs:
- 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 }}
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space:stable,${{ secrets.DOCKERHUB_USERNAME }}/plane-space:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=makeplane/plane-space:latest
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-space:latest
else
TAG=${{ env.SPACE_TAG }}
fi
@@ -241,7 +241,7 @@ jobs:
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
BACKEND_TAG: makeplane/plane-backend:${{ needs.branch_build_setup.outputs.gh_branch_name }}
BACKEND_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/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 }}
@@ -251,9 +251,9 @@ jobs:
- 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 }}
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:stable,${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=makeplane/plane-backend:latest
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-backend:latest
else
TAG=${{ env.BACKEND_TAG }}
fi
@@ -293,7 +293,7 @@ jobs:
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
PROXY_TAG: makeplane/plane-proxy:${{ needs.branch_build_setup.outputs.gh_branch_name }}
PROXY_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/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 }}
@@ -303,9 +303,9 @@ jobs:
- 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 }}
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:stable,${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:${{ github.event.release.tag_name }}
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=makeplane/plane-proxy:latest
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy:latest
else
TAG=${{ env.PROXY_TAG }}
fi
@@ -3,11 +3,10 @@ name: Build and Lint on Pull Request
on:
workflow_dispatch:
pull_request:
types: ["opened", "synchronize", "ready_for_review"]
types: ["opened", "synchronize"]
jobs:
get-changed-files:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
outputs:
apiserver_changed: ${{ steps.changed-files.outputs.apiserver_any_changed }}
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
else
echo "MATCH=false" >> $GITHUB_OUTPUT
fi
Create_PR:
Auto_Merge:
if: ${{ needs.Check_Branch.outputs.BRANCH_MATCH == 'true' }}
needs: [Check_Branch]
runs-on: ubuntu-latest
+124
View File
@@ -0,0 +1,124 @@
FROM node:18-alpine AS builder
RUN apk add --no-cache libc6-compat
# Set working directory
WORKDIR /app
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
RUN yarn global add turbo
RUN apk add tree
COPY . .
RUN turbo prune --scope=app --scope=plane-deploy --docker
CMD tree -I node_modules/
# Add lockfile and package.json's of isolated subworkspace
FROM node:18-alpine AS installer
RUN apk add --no-cache libc6-compat
WORKDIR /app
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
# First install the dependencies (as they change less often)
COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/yarn.lock ./yarn.lock
RUN yarn install
# # Build the project
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
COPY replace-env-vars.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/replace-env-vars.sh
RUN yarn turbo run build
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL}
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
WORKDIR /code
RUN apk --no-cache add \
"libpq~=15" \
"libxslt~=1.1" \
"nodejs-current~=19" \
"xmlsec~=1.2" \
"nginx" \
"nodejs" \
"npm" \
"supervisor"
COPY apiserver/requirements.txt ./
COPY apiserver/requirements ./requirements
RUN apk add --no-cache libffi-dev
RUN apk add --no-cache --virtual .build-deps \
"bash~=5.2" \
"g++~=12.2" \
"gcc~=12.2" \
"cargo~=1.64" \
"git~=2" \
"make~=4.3" \
"postgresql13-dev~=13" \
"libc-dev" \
"linux-headers" \
&& \
pip install -r requirements.txt --compile --no-cache-dir \
&& \
apk del .build-deps
# Add in Django deps and generate Django's static files
COPY apiserver/manage.py manage.py
COPY apiserver/plane plane/
COPY apiserver/templates templates/
RUN apk --no-cache add "bash~=5.2"
COPY apiserver/bin ./bin/
RUN chmod +x ./bin/*
RUN chmod -R 777 /code
# Expose container port and run entry point script
WORKDIR /app
COPY --from=installer /app/apps/app/next.config.js .
COPY --from=installer /app/apps/app/package.json .
COPY --from=installer /app/apps/space/next.config.js .
COPY --from=installer /app/apps/space/package.json .
COPY --from=installer /app/apps/app/.next/standalone ./
COPY --from=installer /app/apps/app/.next/static ./apps/app/.next/static
COPY --from=installer /app/apps/space/.next/standalone ./
COPY --from=installer /app/apps/space/.next ./apps/space/.next
ENV NEXT_TELEMETRY_DISABLED 1
# RUN rm /etc/nginx/conf.d/default.conf
#######################################################################
COPY nginx/nginx-single-docker-image.conf /etc/nginx/http.d/default.conf
#######################################################################
COPY nginx/supervisor.conf /code/supervisor.conf
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
COPY replace-env-vars.sh /usr/local/bin/
COPY start.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/replace-env-vars.sh
RUN chmod +x /usr/local/bin/start.sh
EXPOSE 80
CMD ["supervisord","-c","/code/supervisor.conf"]
-1
View File
@@ -1,4 +1,3 @@
"use client";
import { FC } from "react";
import { useForm } from "react-hook-form";
import { Lightbulb } from "lucide-react";
+5 -5
View File
@@ -1,10 +1,10 @@
"use client";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { Loader } from "@plane/ui";
// components
import { PageHeader } from "@/components/common";
import { PageHeader } from "@/components/core";
// hooks
import { useInstance } from "@/hooks/store";
// components
@@ -19,14 +19,14 @@ const InstanceAIPage = observer(() => {
return (
<>
<PageHeader title="Artificial Intelligence - God Mode" />
<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="relative container mx-auto w-full h-full p-8 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 pb-3 space-y-1 flex-shrink-0">
<div className="text-xl font-medium text-custom-text-100">AI features for all your workspaces</div>
<div className="text-sm font-normal text-custom-text-300">
Configure your AI API credentials so Plane AI features are turned on for all your workspaces.
</div>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
<div className="flex-grow overflow-hidden overflow-y-auto">
{formattedConfig ? (
<InstanceAIForm config={formattedConfig} />
) : (
@@ -1,7 +1,7 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
// hooks
import { TInstanceAuthenticationMethodKeys } from "@plane/types";
import { ToggleSwitch } from "@plane/ui";
@@ -1,7 +1,7 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import Link from "next/link";
// icons
import { Settings2 } from "lucide-react";
@@ -1,7 +1,7 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import Link from "next/link";
// icons
import { Settings2 } from "lucide-react";
@@ -1,6 +1,5 @@
export * from "./email-config-switch";
export * from "./password-config-switch";
export * from "./authentication-method-card";
export * from "./gitlab-config";
export * from "./github-config";
export * from "./google-config";
@@ -1,7 +1,7 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
// hooks
import { TInstanceAuthenticationMethodKeys } from "@plane/types";
import { ToggleSwitch } from "@plane/ui";
-2
View File
@@ -1,5 +1,3 @@
"use client";
import { FC, useState } from "react";
import isEmpty from "lodash/isEmpty";
import Link from "next/link";
+6 -6
View File
@@ -1,14 +1,13 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import Image from "next/image";
import { useTheme } from "next-themes";
import useSWR from "swr";
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
// components
import { AuthenticationMethodCard } from "@/components/authentication";
import { PageHeader } from "@/components/common";
import { PageHeader } from "@/components/core";
// helpers
import { resolveGeneralTheme } from "@/helpers/common.helper";
// hooks
@@ -17,6 +16,7 @@ import { useInstance } from "@/hooks/store";
import githubLightModeImage from "@/public/logos/github-black.png";
import githubDarkModeImage from "@/public/logos/github-white.png";
// local components
import { AuthenticationMethodCard } from "../components";
import { InstanceGithubConfigForm } from "./form";
const InstanceGithubAuthenticationPage = observer(() => {
@@ -64,8 +64,8 @@ const InstanceGithubAuthenticationPage = observer(() => {
return (
<>
<PageHeader title="Authentication - God Mode" />
<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="relative container mx-auto w-full h-full p-8 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 pb-3 space-y-1 flex-shrink-0">
<AuthenticationMethodCard
name="Github"
description="Allow members to login or sign up to plane with their Github accounts."
@@ -93,7 +93,7 @@ const InstanceGithubAuthenticationPage = observer(() => {
withBorder={false}
/>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md p-4">
<div className="flex-grow overflow-hidden overflow-y-auto">
{formattedConfig ? (
<InstanceGithubConfigForm config={formattedConfig} />
) : (
-212
View File
@@ -1,212 +0,0 @@
import { FC, useState } from "react";
import isEmpty from "lodash/isEmpty";
import Link from "next/link";
import { useForm } from "react-hook-form";
// types
import { IFormattedInstanceConfiguration, TInstanceGitlabAuthenticationConfigurationKeys } from "@plane/types";
// ui
import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
// components
import {
ConfirmDiscardModal,
ControllerInput,
CopyField,
TControllerInputFormField,
TCopyField,
} from "@/components/common";
// helpers
import { API_BASE_URL, cn } from "@/helpers/common.helper";
// hooks
import { useInstance } from "@/hooks/store";
type Props = {
config: IFormattedInstanceConfiguration;
};
type GitlabConfigFormValues = Record<TInstanceGitlabAuthenticationConfigurationKeys, string>;
export const InstanceGitlabConfigForm: FC<Props> = (props) => {
const { config } = props;
// states
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
// store hooks
const { updateInstanceConfigurations } = useInstance();
// form data
const {
handleSubmit,
control,
reset,
formState: { errors, isDirty, isSubmitting },
} = useForm<GitlabConfigFormValues>({
defaultValues: {
GITLAB_HOST: config["GITLAB_HOST"],
GITLAB_CLIENT_ID: config["GITLAB_CLIENT_ID"],
GITLAB_CLIENT_SECRET: config["GITLAB_CLIENT_SECRET"],
},
});
const originURL = !isEmpty(API_BASE_URL) ? API_BASE_URL : typeof window !== "undefined" ? window.location.origin : "";
const GITLAB_FORM_FIELDS: TControllerInputFormField[] = [
{
key: "GITLAB_HOST",
type: "text",
label: "Host",
description: (
<>
This is the <b>GitLab host</b> to use for login, <b>including scheme</b>.
</>
),
placeholder: "https://gitlab.com",
error: Boolean(errors.GITLAB_HOST),
required: true,
},
{
key: "GITLAB_CLIENT_ID",
type: "text",
label: "Application ID",
description: (
<>
Get this from your{" "}
<a
tabIndex={-1}
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
GitLab OAuth application settings
</a>
.
</>
),
placeholder: "c2ef2e7fc4e9d15aa7630f5637d59e8e4a27ff01dceebdb26b0d267b9adcf3c3",
error: Boolean(errors.GITLAB_CLIENT_ID),
required: true,
},
{
key: "GITLAB_CLIENT_SECRET",
type: "password",
label: "Secret",
description: (
<>
The client secret is also found in your{" "}
<a
tabIndex={-1}
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
GitLab OAuth application settings
</a>
.
</>
),
placeholder: "gloas-f79cfa9a03c97f6ffab303177a5a6778a53c61e3914ba093412f68a9298a1b28",
error: Boolean(errors.GITLAB_CLIENT_SECRET),
required: true,
},
];
const GITLAB_SERVICE_FIELD: TCopyField[] = [
{
key: "Callback_URL",
label: "Callback URL",
url: `${originURL}/auth/gitlab/callback/`,
description: (
<>
We will auto-generate this. Paste this into the <b>Redirect URI</b> field of your{" "}
<a
tabIndex={-1}
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
target="_blank"
className="text-custom-primary-100 hover:underline"
rel="noreferrer"
>
GitLab OAuth application
</a>
.
</>
),
},
];
const onSubmit = async (formData: GitlabConfigFormValues) => {
const payload: Partial<GitlabConfigFormValues> = { ...formData };
await updateInstanceConfigurations(payload)
.then((response = []) => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success",
message: "GitLab Configuration Settings updated successfully",
});
reset({
GITLAB_HOST: response.find((item) => item.key === "GITLAB_HOST")?.value,
GITLAB_CLIENT_ID: response.find((item) => item.key === "GITLAB_CLIENT_ID")?.value,
GITLAB_CLIENT_SECRET: response.find((item) => item.key === "GITLAB_CLIENT_SECRET")?.value,
});
})
.catch((err) => console.error(err));
};
const handleGoBack = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
if (isDirty) {
e.preventDefault();
setIsDiscardChangesModalOpen(true);
}
};
return (
<>
<ConfirmDiscardModal
isOpen={isDiscardChangesModalOpen}
onDiscardHref="/authentication"
handleClose={() => setIsDiscardChangesModalOpen(false)}
/>
<div className="flex flex-col gap-8">
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1">
<div className="pt-2 text-xl font-medium">Configuration</div>
{GITLAB_FORM_FIELDS.map((field) => (
<ControllerInput
key={field.key}
control={control}
type={field.type}
name={field.key}
label={field.label}
description={field.description}
placeholder={field.placeholder}
error={field.error}
required={field.required}
/>
))}
<div className="flex flex-col gap-1 pt-4">
<div className="flex items-center gap-4">
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isDirty}>
{isSubmitting ? "Saving..." : "Save changes"}
</Button>
<Link
href="/authentication"
className={cn(getButtonStyling("link-neutral", "md"), "font-medium")}
onClick={handleGoBack}
>
Go back
</Link>
</div>
</div>
</div>
<div className="col-span-2 md:col-span-1">
<div className="flex flex-col gap-y-4 px-6 py-4 my-2 bg-custom-background-80/60 rounded-lg">
<div className="pt-2 text-xl font-medium">Service provider details</div>
{GITLAB_SERVICE_FIELD.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))}
</div>
</div>
</div>
</div>
</>
);
};
-101
View File
@@ -1,101 +0,0 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react";
import Image from "next/image";
import useSWR from "swr";
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
// components
import { AuthenticationMethodCard } from "@/components/authentication";
import { PageHeader } from "@/components/common";
// hooks
import { useInstance } from "@/hooks/store";
// icons
import GitlabLogo from "@/public/logos/gitlab-logo.svg";
// local components
import { InstanceGitlabConfigForm } from "./form";
const InstanceGitlabAuthenticationPage = observer(() => {
// store
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
// state
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
// config
const enableGitlabConfig = formattedConfig?.IS_GITLAB_ENABLED ?? "";
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
const updateConfig = async (key: "IS_GITLAB_ENABLED", value: string) => {
setIsSubmitting(true);
const payload = {
[key]: value,
};
const updateConfigPromise = updateInstanceConfigurations(payload);
setPromiseToast(updateConfigPromise, {
loading: "Saving Configuration...",
success: {
title: "Configuration saved",
message: () => `GitLab authentication is now ${value ? "active" : "disabled"}.`,
},
error: {
title: "Error",
message: () => "Failed to save configuration",
},
});
await updateConfigPromise
.then(() => {
setIsSubmitting(false);
})
.catch((err) => {
console.error(err);
setIsSubmitting(false);
});
};
return (
<>
<PageHeader title="Authentication - God Mode" />
<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">
<AuthenticationMethodCard
name="GitLab"
description="Allow members to login or sign up to plane with their GitLab accounts."
icon={<Image src={GitlabLogo} height={24} width={24} alt="GitLab Logo" />}
config={
<ToggleSwitch
value={Boolean(parseInt(enableGitlabConfig))}
onChange={() => {
Boolean(parseInt(enableGitlabConfig)) === true
? updateConfig("IS_GITLAB_ENABLED", "0")
: updateConfig("IS_GITLAB_ENABLED", "1");
}}
size="sm"
disabled={isSubmitting || !formattedConfig}
/>
}
disabled={isSubmitting || !formattedConfig}
withBorder={false}
/>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md p-4">
{formattedConfig ? (
<InstanceGitlabConfigForm config={formattedConfig} />
) : (
<Loader className="space-y-8">
<Loader.Item height="50px" width="25%" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" width="50%" />
</Loader>
)}
</div>
</div>
</>
);
});
export default InstanceGitlabAuthenticationPage;
-1
View File
@@ -1,4 +1,3 @@
"use client";
import { FC, useState } from "react";
import isEmpty from "lodash/isEmpty";
import Link from "next/link";
+6 -6
View File
@@ -1,18 +1,18 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import Image from "next/image";
import useSWR from "swr";
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
// components
import { AuthenticationMethodCard } from "@/components/authentication";
import { PageHeader } from "@/components/common";
import { PageHeader } from "@/components/core";
// hooks
import { useInstance } from "@/hooks/store";
// icons
import GoogleLogo from "@/public/logos/google-logo.svg";
// local components
import { AuthenticationMethodCard } from "../components";
import { InstanceGoogleConfigForm } from "./form";
const InstanceGoogleAuthenticationPage = observer(() => {
@@ -58,8 +58,8 @@ const InstanceGoogleAuthenticationPage = observer(() => {
return (
<>
<PageHeader title="Authentication - God Mode" />
<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="relative container mx-auto w-full h-full p-8 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 pb-3 space-y-1 flex-shrink-0">
<AuthenticationMethodCard
name="Google"
description="Allow members to login or sign up to plane with their Google
@@ -81,7 +81,7 @@ const InstanceGoogleAuthenticationPage = observer(() => {
withBorder={false}
/>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md p-4">
<div className="flex-grow overflow-hidden overflow-y-auto">
{formattedConfig ? (
<InstanceGoogleConfigForm config={formattedConfig} />
) : (
+84 -41
View File
@@ -1,17 +1,39 @@
"use client";
import { useState } from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import Image from "next/image";
import { useTheme } from "next-themes";
import useSWR from "swr";
import { Mails, KeyRound } from "lucide-react";
import { TInstanceConfigurationKeys } from "@plane/types";
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
import { PageHeader } from "@/components/common";
// helpers
import { cn } from "@/helpers/common.helper";
import { Loader, setPromiseToast } from "@plane/ui";
// components
import { PageHeader } from "@/components/core";
// hooks
// helpers
import { resolveGeneralTheme } from "@/helpers/common.helper";
import { useInstance } from "@/hooks/store";
// plane admin components
import { AuthenticationModes } from "@/plane-admin/components/authentication";
// images
import githubLightModeImage from "@/public/logos/github-black.png";
import githubDarkModeImage from "@/public/logos/github-white.png";
import GoogleLogo from "@/public/logos/google-logo.svg";
// local components
import {
AuthenticationMethodCard,
EmailCodesConfiguration,
PasswordLoginConfiguration,
GithubConfiguration,
GoogleConfiguration,
} from "./components";
type TInstanceAuthenticationMethodCard = {
key: string;
name: string;
description: string;
icon: JSX.Element;
config: JSX.Element;
};
const InstanceAuthenticationPage = observer(() => {
// store
@@ -21,8 +43,8 @@ const InstanceAuthenticationPage = observer(() => {
// state
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
// derived values
const enableSignUpConfig = formattedConfig?.ENABLE_SIGNUP ?? "";
// theme
const { resolvedTheme } = useTheme();
const updateConfig = async (key: TInstanceConfigurationKeys, value: string) => {
setIsSubmitting(true);
@@ -55,48 +77,69 @@ const InstanceAuthenticationPage = observer(() => {
});
};
// Authentication methods
const authenticationMethodsCard: TInstanceAuthenticationMethodCard[] = [
{
key: "email-codes",
name: "Email codes",
description: "Login or sign up using codes sent via emails. You need to have email setup here and enabled.",
icon: <Mails className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
config: <EmailCodesConfiguration disabled={isSubmitting} updateConfig={updateConfig} />,
},
{
key: "password-login",
name: "Password based login",
description: "Allow members to create accounts with passwords for emails to sign in.",
icon: <KeyRound className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
config: <PasswordLoginConfiguration disabled={isSubmitting} updateConfig={updateConfig} />,
},
{
key: "google",
name: "Google",
description: "Allow members to login or sign up to plane with their Google accounts.",
icon: <Image src={GoogleLogo} height={20} width={20} alt="Google Logo" />,
config: <GoogleConfiguration disabled={isSubmitting} updateConfig={updateConfig} />,
},
{
key: "github",
name: "Github",
description: "Allow members to login or sign up to plane with their Github accounts.",
icon: (
<Image
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
height={20}
width={20}
alt="GitHub Logo"
/>
),
config: <GithubConfiguration disabled={isSubmitting} updateConfig={updateConfig} />,
},
];
return (
<>
<PageHeader title="Authentication - God Mode" />
<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="relative container mx-auto w-full h-full p-8 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 pb-3 space-y-1 flex-shrink-0">
<div className="text-xl font-medium text-custom-text-100">Manage authentication 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.
</div>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
<div className="flex-grow overflow-hidden overflow-y-auto">
{formattedConfig ? (
<div className="space-y-3">
<div className="text-lg font-medium pb-1">Sign-up configuration</div>
<div className={cn("w-full flex items-center gap-14 rounded")}>
<div className="flex grow items-center gap-4">
<div className="grow">
<div className={cn("font-medium leading-5 text-custom-text-100 text-sm")}>
Allow anyone to sign up without invite
</div>
<div className={cn("font-normal leading-5 text-custom-text-300 text-xs")}>
Toggling this off will disable self sign ups.
</div>
</div>
</div>
<div className={`shrink-0 pr-4 ${isSubmitting && "opacity-70"}`}>
<div className="flex items-center gap-4">
<ToggleSwitch
value={Boolean(parseInt(enableSignUpConfig))}
onChange={() => {
Boolean(parseInt(enableSignUpConfig)) === true
? updateConfig("ENABLE_SIGNUP", "0")
: updateConfig("ENABLE_SIGNUP", "1");
}}
size="sm"
disabled={isSubmitting}
/>
</div>
</div>
</div>
<div className="text-lg font-medium pt-6">Authentication modes</div>
<AuthenticationModes disabled={isSubmitting} updateConfig={updateConfig} />
<div className="text-lg font-medium">Authentication modes</div>
{authenticationMethodsCard.map((method) => (
<AuthenticationMethodCard
key={method.key}
name={method.name}
description={method.description}
icon={method.icon}
config={method.config}
disabled={isSubmitting}
/>
))}
</div>
) : (
<Loader className="space-y-10">
-2
View File
@@ -1,5 +1,3 @@
"use client";
import React, { FC, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
// types
+3 -3
View File
@@ -10,6 +10,6 @@ export const metadata: Metadata = {
title: "Email Settings - God Mode",
};
export default function EmailLayout({ children }: EmailLayoutProps) {
return <AdminLayout>{children}</AdminLayout>;
}
const EmailLayout = ({ children }: EmailLayoutProps) => <AdminLayout>{children}</AdminLayout>;
export default EmailLayout;
+5 -5
View File
@@ -1,10 +1,10 @@
"use client";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { Loader } from "@plane/ui";
// components
import { PageHeader } from "@/components/common";
import { PageHeader } from "@/components/core";
// hooks
import { useInstance } from "@/hooks/store";
// components
@@ -19,8 +19,8 @@ const InstanceEmailPage = observer(() => {
return (
<>
<PageHeader title="Email - God Mode" />
<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="relative container mx-auto w-full h-full p-8 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 pb-3 space-y-1 flex-shrink-0">
<div className="text-xl font-medium text-custom-text-100">Secure emails from your own instance</div>
<div className="text-sm font-normal text-custom-text-300">
Plane can send useful emails to you and your users from your own instance without talking to the Internet.
@@ -30,7 +30,7 @@ const InstanceEmailPage = observer(() => {
</div>
</div>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
<div className="flex-grow overflow-hidden overflow-y-auto">
{formattedConfig ? (
<InstanceEmailForm config={formattedConfig} />
) : (
+1 -1
View File
@@ -51,7 +51,7 @@ export const SendTestEmailModal: FC<Props> = (props) => {
setSendEmailStep(ESendEmailSteps.SUCCESS);
})
.catch((error) => {
setError(error?.error || "Failed to send email");
setError(error?.message || "Failed to send email");
setSendEmailStep(ESendEmailSteps.FAILED);
})
.finally(() => {
+1 -1
View File
@@ -1,6 +1,6 @@
"use client";
import { FC } from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import { Controller, useForm } from "react-hook-form";
import { Telescope } from "lucide-react";
// types
+4 -4
View File
@@ -1,5 +1,5 @@
"use client";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
// hooks
import { useInstance } from "@/hooks/store";
// components
@@ -10,15 +10,15 @@ function GeneralPage() {
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">
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<div className="relative container mx-auto w-full h-full p-8 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 pb-3 space-y-1 flex-shrink-0">
<div className="text-xl font-medium text-custom-text-100">General settings</div>
<div className="text-sm font-normal text-custom-text-300">
Change the name of your instance and instance admin e-mail addresses. Enable or disable telemetry in your
instance.
</div>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
<div className="flex-grow overflow-hidden overflow-y-auto">
{instance && instanceAdmins && (
<GeneralConfigurationForm instance={instance} instanceAdmins={instanceAdmins} />
)}
+27 -75
View File
@@ -332,90 +332,42 @@ body {
}
/* scrollbar style */
@-moz-document url-prefix() {
* {
scrollbar-width: none;
}
.vertical-scrollbar,
.horizontal-scrollbar {
scrollbar-width: initial;
scrollbar-color: rgba(96, 100, 108, 0.1) transparent;
}
.vertical-scrollbar:hover,
.horizontal-scrollbar:hover {
scrollbar-color: rgba(96, 100, 108, 0.25) transparent;
}
.vertical-scrollbar:active,
.horizontal-scrollbar:active {
scrollbar-color: rgba(96, 100, 108, 0.7) transparent;
}
::-webkit-scrollbar {
display: none;
}
.vertical-scrollbar {
overflow-y: auto;
.horizontal-scroll-enable {
overflow-x: scroll;
}
.horizontal-scrollbar {
overflow-x: auto;
}
.vertical-scrollbar::-webkit-scrollbar,
.horizontal-scrollbar::-webkit-scrollbar {
.horizontal-scroll-enable::-webkit-scrollbar {
display: block;
}
.vertical-scrollbar::-webkit-scrollbar-track,
.horizontal-scrollbar::-webkit-scrollbar-track {
background-color: transparent;
border-radius: 9999px;
}
.vertical-scrollbar::-webkit-scrollbar-thumb,
.horizontal-scrollbar::-webkit-scrollbar-thumb {
background-clip: padding-box;
background-color: rgba(96, 100, 108, 0.1);
border-radius: 9999px;
}
.vertical-scrollbar:hover::-webkit-scrollbar-thumb,
.horizontal-scrollbar:hover::-webkit-scrollbar-thumb {
background-color: rgba(96, 100, 108, 0.25);
}
.vertical-scrollbar::-webkit-scrollbar-thumb:hover,
.horizontal-scrollbar::-webkit-scrollbar-thumb:hover {
background-color: rgba(96, 100, 108, 0.5);
}
.vertical-scrollbar::-webkit-scrollbar-thumb:active,
.horizontal-scrollbar::-webkit-scrollbar-thumb:active {
background-color: rgba(96, 100, 108, 0.7);
}
.vertical-scrollbar::-webkit-scrollbar-corner,
.horizontal-scrollbar::-webkit-scrollbar-corner {
background-color: transparent;
}
.vertical-scrollbar-margin-top-md::-webkit-scrollbar-track {
margin-top: 44px;
height: 7px;
width: 0;
}
/* scrollbar sm size */
.scrollbar-sm::-webkit-scrollbar {
height: 12px;
width: 12px;
.horizontal-scroll-enable::-webkit-scrollbar-track {
height: 7px;
background-color: rgba(var(--color-background-100));
}
.scrollbar-sm::-webkit-scrollbar-thumb {
border: 3px solid rgba(0, 0, 0, 0);
}
/* scrollbar md size */
.scrollbar-md::-webkit-scrollbar {
height: 14px;
width: 14px;
}
.scrollbar-md::-webkit-scrollbar-thumb {
border: 3px solid rgba(0, 0, 0, 0);
}
/* scrollbar lg size */
.scrollbar-lg::-webkit-scrollbar {
height: 16px;
width: 16px;
.horizontal-scroll-enable::-webkit-scrollbar-thumb {
border-radius: 5px;
background-color: rgba(var(--color-scrollbar));
}
.scrollbar-lg::-webkit-scrollbar-thumb {
border: 4px solid rgba(0, 0, 0, 0);
.vertical-scroll-enable::-webkit-scrollbar {
display: block;
width: 5px;
}
.vertical-scroll-enable::-webkit-scrollbar-track {
width: 5px;
}
.vertical-scroll-enable::-webkit-scrollbar-thumb {
border-radius: 5px;
background-color: rgba(var(--color-background-90));
}
/* end scrollbar style */
-1
View File
@@ -1,4 +1,3 @@
"use client";
import { FC } from "react";
import { useForm } from "react-hook-form";
import { IFormattedInstanceConfiguration, TInstanceImageConfigurationKeys } from "@plane/types";
+3 -3
View File
@@ -10,6 +10,6 @@ export const metadata: Metadata = {
title: "Images Settings - God Mode",
};
export default function ImageLayout({ children }: ImageLayoutProps) {
return <AdminLayout>{children}</AdminLayout>;
}
const ImageLayout = ({ children }: ImageLayoutProps) => <AdminLayout>{children}</AdminLayout>;
export default ImageLayout;
+5 -5
View File
@@ -1,10 +1,10 @@
"use client";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { Loader } from "@plane/ui";
// components
import { PageHeader } from "@/components/common";
import { PageHeader } from "@/components/core";
// hooks
import { useInstance } from "@/hooks/store";
// local
@@ -19,14 +19,14 @@ const InstanceImagePage = observer(() => {
return (
<>
<PageHeader title="Image - God Mode" />
<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="relative container mx-auto w-full h-full p-8 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 pb-3 space-y-1 flex-shrink-0">
<div className="text-xl font-medium text-custom-text-100">Third-party image libraries</div>
<div className="text-sm font-normal text-custom-text-300">
Let your users search and choose images from third-party libraries
</div>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
<div className="flex-grow overflow-hidden overflow-y-auto">
{formattedConfig ? (
<InstanceImageConfigForm config={formattedConfig} />
) : (
+5 -5
View File
@@ -16,12 +16,10 @@ import { UserProvider } from "@/lib/user-provider";
// styles
import "./globals.css";
const ToastWithTheme = () => {
function RootLayout({ children }: { children: ReactNode }) {
// themes
const { resolvedTheme } = useTheme();
return <Toast theme={resolveGeneralTheme(resolvedTheme)} />;
};
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<head>
@@ -33,7 +31,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
</head>
<body className={`antialiased`}>
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
<ToastWithTheme />
<Toast theme={resolveGeneralTheme(resolvedTheme)} />
<SWRConfig value={SWR_CONFIG}>
<StoreProvider>
<InstanceProvider>
@@ -46,3 +44,5 @@ export default function RootLayout({ children }: { children: ReactNode }) {
</html>
);
}
export default RootLayout;
@@ -1,104 +0,0 @@
import { observer } from "mobx-react";
import Image from "next/image";
import { useTheme } from "next-themes";
import { KeyRound, Mails } from "lucide-react";
// types
import { TInstanceAuthenticationMethodKeys, TInstanceAuthenticationModes } from "@plane/types";
// components
import {
AuthenticationMethodCard,
EmailCodesConfiguration,
GithubConfiguration,
GitlabConfiguration,
GoogleConfiguration,
PasswordLoginConfiguration,
} from "@/components/authentication";
// helpers
import { resolveGeneralTheme } from "@/helpers/common.helper";
// images
import githubLightModeImage from "@/public/logos/github-black.png";
import githubDarkModeImage from "@/public/logos/github-white.png";
import GitlabLogo from "@/public/logos/gitlab-logo.svg";
import GoogleLogo from "@/public/logos/google-logo.svg";
export type TAuthenticationModeProps = {
disabled: boolean;
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
};
export type TGetAuthenticationModeProps = {
disabled: boolean;
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
resolvedTheme: string | undefined;
};
// Authentication methods
export const getAuthenticationModes: (props: TGetAuthenticationModeProps) => TInstanceAuthenticationModes[] = ({
disabled,
updateConfig,
resolvedTheme,
}) => [
{
key: "email-codes",
name: "Email codes",
description: "Login or sign up using codes sent via emails. You need to have email setup here and enabled.",
icon: <Mails className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
config: <EmailCodesConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
key: "password-login",
name: "Password based login",
description: "Allow members to create accounts with passwords for emails to sign in.",
icon: <KeyRound className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
config: <PasswordLoginConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
key: "google",
name: "Google",
description: "Allow members to login or sign up to plane with their Google accounts.",
icon: <Image src={GoogleLogo} height={20} width={20} alt="Google Logo" />,
config: <GoogleConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
key: "github",
name: "Github",
description: "Allow members to login or sign up to plane with their Github accounts.",
icon: (
<Image
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
height={20}
width={20}
alt="GitHub Logo"
/>
),
config: <GithubConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
{
key: "gitlab",
name: "GitLab",
description: "Allow members to login or sign up to plane with their GitLab accounts.",
icon: <Image src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />,
config: <GitlabConfiguration disabled={disabled} updateConfig={updateConfig} />,
},
];
export const AuthenticationModes: React.FC<TAuthenticationModeProps> = observer((props) => {
const { disabled, updateConfig } = props;
// next-themes
const { resolvedTheme } = useTheme();
return (
<>
{getAuthenticationModes({ disabled, updateConfig, resolvedTheme }).map((method) => (
<AuthenticationMethodCard
key={method.key}
name={method.name}
description={method.description}
icon={method.icon}
config={method.config}
disabled={disabled}
/>
))}
</>
);
});
@@ -1 +0,0 @@
export * from "./authentication-modes";
@@ -1,15 +1,13 @@
"use client";
import { FC, useState, useRef } from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import Link from "next/link";
import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
import { Transition } from "@headlessui/react";
// ui
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
// helpers
import { WEB_BASE_URL, cn } from "@/helpers/common.helper";
// hooks
import { WEB_BASE_URL } from "@/helpers/common.helper";
import { useTheme } from "@/hooks/store";
// assets
import packageJson from "package.json";
@@ -40,16 +38,13 @@ export const HelpSection: FC = observer(() => {
// refs
const helpOptionsRef = useRef<HTMLDivElement | null>(null);
const redirectionLink = encodeURI(WEB_BASE_URL + "/");
const redirectionLink = encodeURI(WEB_BASE_URL + "/create-workspace");
return (
<div
className={cn(
"flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 px-4 h-14 flex-shrink-0",
{
"flex-col h-auto py-1.5": isSidebarCollapsed,
}
)}
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-4 py-2 ${
isSidebarCollapsed ? "flex-col" : ""
}`}
>
<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}>
@@ -1,14 +1,14 @@
"use client";
import { FC, useEffect, useRef } from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
// hooks
import { HelpSection, SidebarMenu, SidebarDropdown } from "@/components/admin-sidebar";
import { useTheme } from "@/hooks/store";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
export interface IInstanceSidebar { }
export interface IInstanceSidebar {}
export const InstanceSidebar: FC<IInstanceSidebar> = observer(() => {
// store
@@ -41,10 +41,10 @@ export const InstanceSidebar: FC<IInstanceSidebar> = observer(() => {
<div
className={`inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100 duration-300
fixed md:relative
${isSidebarCollapsed ? "-ml-[250px]" : ""}
sm:${isSidebarCollapsed ? "-ml-[250px]" : ""}
md:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[250px]"}
lg:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[250px]"}
${isSidebarCollapsed ? "-ml-[280px]" : ""}
sm:${isSidebarCollapsed ? "-ml-[280px]" : ""}
md:ml-0 ${isSidebarCollapsed ? "w-[80px]" : "w-[280px]"}
lg:ml-0 ${isSidebarCollapsed ? "w-[80px]" : "w-[280px]"}
`}
>
<div ref={ref} className="flex h-full w-full flex-1 flex-col">
@@ -1,7 +1,7 @@
"use client";
import { Fragment, useEffect, useState } from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import { useTheme as useNextTheme } from "next-themes";
import { LogOut, UserCog2, Palette } from "lucide-react";
import { Menu, Transition } from "@headlessui/react";
@@ -1,7 +1,7 @@
"use client";
import { FC } from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
// hooks
import { Menu } from "lucide-react";
import { useTheme } from "@/hooks/store";
@@ -1,6 +1,6 @@
"use client";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
@@ -56,7 +56,7 @@ export const SidebarMenu = observer(() => {
};
return (
<div className="flex h-full w-full flex-col gap-2.5 overflow-y-scroll vertical-scrollbar scrollbar-sm px-4 py-4">
<div className="flex h-full w-full flex-col gap-2.5 overflow-y-auto px-4 py-4">
{INSTANCE_ADMIN_LINKS.map((item, index) => {
const isActive = item.href === pathName || pathName.includes(item.href);
return (
@@ -1,7 +1,7 @@
"use client";
import { FC } from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import { usePathname } from "next/navigation";
// mobx
// ui
@@ -10,7 +10,7 @@ import { Settings } from "lucide-react";
import { Breadcrumbs } from "@plane/ui";
// components
import { SidebarHamburgerToggle } from "@/components/admin-sidebar";
import { BreadcrumbLink } from "@/components/common";
import { BreadcrumbLink } from "components/common";
export const InstanceHeader: FC = observer(() => {
const pathName = usePathname();
@@ -31,8 +31,6 @@ export const InstanceHeader: FC = observer(() => {
return "Google";
case "github":
return "Github";
case "gitlab":
return "GitLab";
default:
return pathName.toUpperCase();
}
@@ -1,5 +1,3 @@
"use client";
import Link from "next/link";
import { Tooltip } from "@plane/ui";
@@ -1,5 +1,3 @@
"use client";
import React from "react";
import Link from "next/link";
// headless ui
@@ -45,22 +43,33 @@ export const ConfirmDiscardModal: React.FC<Props> = (props) => {
<div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:text-left">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-300">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-custom-text-300"
>
You have unsaved changes
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-custom-text-400">
Changes you made will be lost if you go back. Do you wish to go back?
Changes you made will be lost if you go back. Do you
wish to go back?
</p>
</div>
</div>
</div>
</div>
<div className="flex justify-end items-center p-4 sm:px-6 gap-2">
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
<Button
variant="neutral-primary"
size="sm"
onClick={handleClose}
>
Keep editing
</Button>
<Link href={onDiscardHref} className={getButtonStyling("primary", "sm")}>
<Link
href={onDiscardHref}
className={getButtonStyling("primary", "sm")}
>
Go back
</Link>
</div>
@@ -24,7 +24,7 @@ export const CopyField: React.FC<Props> = (props) => {
return (
<div className="flex flex-col gap-1">
<h4 className="text-sm text-custom-text-200">{label}</h4>
<h4 className="text-sm text-custom-text-300">{label}</h4>
<Button
variant="neutral-primary"
className="flex items-center justify-between py-2"
@@ -40,7 +40,7 @@ export const CopyField: React.FC<Props> = (props) => {
<p className="text-sm font-medium">{url}</p>
<Copy size={18} color="#B9B9B9" />
</Button>
<div className="text-xs text-custom-text-300">{description}</div>
<div className="text-xs text-custom-text-400">{description}</div>
</div>
);
};
@@ -1,5 +1,3 @@
"use client";
import React from "react";
import Image from "next/image";
import { Button } from "@plane/ui";
@@ -6,4 +6,4 @@ export * from "./password-strength-meter";
export * from "./banner";
export * from "./empty-state";
export * from "./logo-spinner";
export * from "./page-header";
export * from "./toast";
@@ -0,0 +1,69 @@
"use client";
// helpers
import { CircleCheck } from "lucide-react";
import { cn } from "@/helpers/common.helper";
import { getPasswordStrength } from "@/helpers/password.helper";
// icons
type Props = {
password: string;
};
export const PasswordStrengthMeter: React.FC<Props> = (props: Props) => {
const { password } = props;
const strength = getPasswordStrength(password);
let bars = [];
let text = "";
let textColor = "";
if (password.length === 0) {
bars = [`bg-[#F0F0F3]`, `bg-[#F0F0F3]`, `bg-[#F0F0F3]`];
text = "Password requirements";
} else if (password.length < 8) {
bars = [`bg-[#DC3E42]`, `bg-[#F0F0F3]`, `bg-[#F0F0F3]`];
text = "Password is too short";
textColor = `text-[#DC3E42]`;
} else if (strength < 3) {
bars = [`bg-[#FFBA18]`, `bg-[#FFBA18]`, `bg-[#F0F0F3]`];
text = "Password is weak";
textColor = `text-[#FFBA18]`;
} else {
bars = [`bg-[#3E9B4F]`, `bg-[#3E9B4F]`, `bg-[#3E9B4F]`];
text = "Password is strong";
textColor = `text-[#3E9B4F]`;
}
const criteria = [
{ label: "Min 8 characters", isValid: password.length >= 8 },
{ label: "Min 1 upper-case letter", isValid: /[A-Z]/.test(password) },
{ label: "Min 1 number", isValid: /\d/.test(password) },
{ label: "Min 1 special character", isValid: /[!@#$%^&*]/.test(password) },
];
return (
<div className="w-full">
<div className="flex w-full gap-1.5">
{bars.map((color, index) => (
<div key={index} className={cn("w-full h-1 rounded-full", color)} />
))}
</div>
<p className={cn("text-xs font-medium py-1", textColor)}>{text}</p>
<div className="flex flex-wrap gap-x-4 gap-y-2">
{criteria.map((criterion, index) => (
<div
key={index}
className={cn(
"flex items-center gap-1 text-xs font-medium",
criterion.isValid ? `text-[#3E9B4F]` : "text-custom-text-400"
)}
>
<CircleCheck width={14} height={14} />
{criterion.label}
</div>
))}
</div>
</div>
);
};
+11
View File
@@ -0,0 +1,11 @@
import { useTheme } from "next-themes";
// ui
import { Toast as ToastComponent } from "@plane/ui";
// helpers
import { resolveGeneralTheme } from "@/helpers/common.helper";
export const Toast = () => {
const { theme } = useTheme();
return <ToastComponent theme={resolveGeneralTheme(theme)} />;
};
+1
View File
@@ -0,0 +1 @@
export * from "./page-header";
@@ -10,7 +10,7 @@ import { Button, Checkbox, Input, Spinner } from "@plane/ui";
import { Banner, PasswordStrengthMeter } from "@/components/common";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
import { E_PASSWORD_STRENGTH, getPasswordStrength } from "@/helpers/password.helper";
import { getPasswordStrength } from "@/helpers/password.helper";
// services
import { AuthService } from "@/services/auth.service";
@@ -121,7 +121,7 @@ export const InstanceSetupForm: FC = (props) => {
formData.first_name &&
formData.email &&
formData.password &&
getPasswordStrength(formData.password) === E_PASSWORD_STRENGTH.STRENGTH_VALID &&
getPasswordStrength(formData.password) >= 3 &&
formData.password === formData.confirm_password
? false
: true,
@@ -158,7 +158,6 @@ export const InstanceSetupForm: FC = (props) => {
onError={() => setIsSubmitting(false)}
>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<input type="hidden" name="is_telemetry_enabled" value={formData.is_telemetry_enabled ? "True" : "False"} />
<div className="flex flex-col sm:flex-row items-center gap-4">
<div className="w-full space-y-1">
@@ -271,7 +270,7 @@ export const InstanceSetupForm: FC = (props) => {
{errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD && errorData.message && (
<p className="px-1 text-xs text-red-500">{errorData.message}</p>
)}
<PasswordStrengthMeter password={formData.password} isFocused={isPasswordInputFocused} />
{isPasswordInputFocused && <PasswordStrengthMeter password={formData.password} />}
</div>
<div className="w-full space-y-1">
@@ -319,9 +318,9 @@ export const InstanceSetupForm: FC = (props) => {
<div className="relative flex items-center pt-2 gap-2">
<div>
<Checkbox
className="w-4 h-4"
iconClassName="w-3 h-3"
id="is_telemetry_enabled"
name="is_telemetry_enabled"
value={formData.is_telemetry_enabled ? "True" : "False"}
onChange={() => handleFormChange("is_telemetry_enabled", !formData.is_telemetry_enabled)}
checked={formData.is_telemetry_enabled}
/>
@@ -1,15 +1,15 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import Image from "next/image";
import { useTheme as nextUseTheme } from "next-themes";
// ui
import { Button, getButtonStyling } from "@plane/ui";
// helpers
import { WEB_BASE_URL, resolveGeneralTheme } from "helpers/common.helper";
import { resolveGeneralTheme } from "helpers/common.helper";
// hooks
import { useTheme } from "@/hooks/store";
import { useInstance, useTheme } from "@/hooks/store";
// icons
import TakeoffIconLight from "/public/logos/takeoff-icon-light.svg";
import TakeoffIconDark from "/public/logos/takeoff-icon-dark.svg";
@@ -17,10 +17,11 @@ import TakeoffIconDark from "/public/logos/takeoff-icon-dark.svg";
export const NewUserPopup: React.FC = observer(() => {
// hooks
const { isNewUserPopup, toggleNewUserPopup } = useTheme();
const { config } = useInstance();
// theme
const { resolvedTheme } = nextUseTheme();
const redirectionLink = encodeURI(WEB_BASE_URL + "/create-workspace");
const redirectionLink = `${config?.app_base_url ? `${config?.app_base_url}/create-workspace` : `/god-mode/`}`;
if (!isNewUserPopup) return <></>;
return (
@@ -1,59 +0,0 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import Link from "next/link";
// icons
import { Settings2 } from "lucide-react";
// types
import { TInstanceAuthenticationMethodKeys } from "@plane/types";
// ui
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useInstance } from "@/hooks/store";
type Props = {
disabled: boolean;
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
};
export const GitlabConfiguration: React.FC<Props> = observer((props) => {
const { disabled, updateConfig } = props;
// store
const { formattedConfig } = useInstance();
// derived values
const enableGitlabConfig = formattedConfig?.IS_GITLAB_ENABLED ?? "";
const isGitlabConfigured = !!formattedConfig?.GITLAB_CLIENT_ID && !!formattedConfig?.GITLAB_CLIENT_SECRET;
return (
<>
{isGitlabConfigured ? (
<div className="flex items-center gap-4">
<Link href="/authentication/gitlab" className={cn(getButtonStyling("link-primary", "md"), "font-medium")}>
Edit
</Link>
<ToggleSwitch
value={Boolean(parseInt(enableGitlabConfig))}
onChange={() => {
Boolean(parseInt(enableGitlabConfig)) === true
? updateConfig("IS_GITLAB_ENABLED", "0")
: updateConfig("IS_GITLAB_ENABLED", "1");
}}
size="sm"
disabled={disabled}
/>
</div>
) : (
<Link
href="/authentication/gitlab"
className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}
>
<Settings2 className="h-4 w-4 p-0.5 text-custom-text-300/80" />
Configure
</Link>
)}
</>
);
});
@@ -1,94 +0,0 @@
"use client";
import { FC, useMemo } from "react";
// import { CircleCheck } from "lucide-react";
// helpers
import { cn } from "@/helpers/common.helper";
import {
E_PASSWORD_STRENGTH,
// PASSWORD_CRITERIA,
getPasswordStrength,
} from "@/helpers/password.helper";
type TPasswordStrengthMeter = {
password: string;
isFocused?: boolean;
};
export const PasswordStrengthMeter: FC<TPasswordStrengthMeter> = (props) => {
const { password, isFocused = false } = props;
// derived values
const strength = useMemo(() => getPasswordStrength(password), [password]);
const strengthBars = useMemo(() => {
switch (strength) {
case E_PASSWORD_STRENGTH.EMPTY: {
return {
bars: [`bg-custom-text-100`, `bg-custom-text-100`, `bg-custom-text-100`],
text: "Please enter your password.",
textColor: "text-custom-text-100",
};
}
case E_PASSWORD_STRENGTH.LENGTH_NOT_VALID: {
return {
bars: [`bg-red-500`, `bg-custom-text-100`, `bg-custom-text-100`],
text: "Password length should me more than 8 characters.",
textColor: "text-red-500",
};
}
case E_PASSWORD_STRENGTH.STRENGTH_NOT_VALID: {
return {
bars: [`bg-red-500`, `bg-custom-text-100`, `bg-custom-text-100`],
text: "Password is weak.",
textColor: "text-red-500",
};
}
case E_PASSWORD_STRENGTH.STRENGTH_VALID: {
return {
bars: [`bg-green-500`, `bg-green-500`, `bg-green-500`],
text: "Password is strong.",
textColor: "text-green-500",
};
}
default: {
return {
bars: [`bg-custom-text-100`, `bg-custom-text-100`, `bg-custom-text-100`],
text: "Please enter your password.",
textColor: "text-custom-text-100",
};
}
}
}, [strength]);
const isPasswordMeterVisible = isFocused ? true : strength === E_PASSWORD_STRENGTH.STRENGTH_VALID ? false : true;
if (!isPasswordMeterVisible) return <></>;
return (
<div className="w-full space-y-2 pt-2">
<div className="space-y-1.5">
<div className="relative flex items-center gap-2">
{strengthBars?.bars.map((color, index) => (
<div key={`${color}-${index}`} className={cn("w-full h-1 rounded-full", color)} />
))}
</div>
<div className={cn(`text-xs font-medium text-custom-text-100`, strengthBars?.textColor)}>
{strengthBars?.text}
</div>
</div>
{/* <div className="relative flex flex-wrap gap-x-4 gap-y-2">
{PASSWORD_CRITERIA.map((criteria) => (
<div
key={criteria.key}
className={cn(
"relative flex items-center gap-1 text-xs",
criteria.isCriteriaValid(password) ? `text-green-500/70` : "text-custom-text-300"
)}
>
<CircleCheck width={14} height={14} />
{criteria.label}
</div>
))}
</div> */}
</div>
);
};
@@ -1 +0,0 @@
export * from "ce/components/authentication/authentication-modes";
@@ -1 +0,0 @@
export * from "./authentication-modes";
+10 -61
View File
@@ -1,67 +1,16 @@
import zxcvbn from "zxcvbn";
export enum E_PASSWORD_STRENGTH {
EMPTY = "empty",
LENGTH_NOT_VALID = "length_not_valid",
STRENGTH_NOT_VALID = "strength_not_valid",
STRENGTH_VALID = "strength_valid",
}
export const isPasswordCriteriaMet = (password: string) => {
const criteria = [password.length >= 8, /[A-Z]/.test(password), /\d/.test(password), /[!@#$%^&*]/.test(password)];
const PASSWORD_MIN_LENGTH = 8;
// const PASSWORD_NUMBER_REGEX = /\d/;
// const PASSWORD_CHAR_CAPS_REGEX = /[A-Z]/;
// const PASSWORD_SPECIAL_CHAR_REGEX = /[`!@#$%^&*()_\-+=\[\]{};':"\\|,.<>\/?~ ]/;
return criteria.every((criterion) => criterion);
};
export const PASSWORD_CRITERIA = [
{
key: "min_8_char",
label: "Min 8 characters",
isCriteriaValid: (password: string) => password.length >= PASSWORD_MIN_LENGTH,
},
// {
// key: "min_1_upper_case",
// label: "Min 1 upper-case letter",
// isCriteriaValid: (password: string) => PASSWORD_NUMBER_REGEX.test(password),
// },
// {
// key: "min_1_number",
// label: "Min 1 number",
// isCriteriaValid: (password: string) => PASSWORD_CHAR_CAPS_REGEX.test(password),
// },
// {
// key: "min_1_special_char",
// label: "Min 1 special character",
// isCriteriaValid: (password: string) => PASSWORD_SPECIAL_CHAR_REGEX.test(password),
// },
];
export const getPasswordStrength = (password: string) => {
if (password.length === 0) return 0;
if (password.length < 8) return 1;
if (!isPasswordCriteriaMet(password)) return 2;
export const getPasswordStrength = (password: string): E_PASSWORD_STRENGTH => {
let passwordStrength: E_PASSWORD_STRENGTH = E_PASSWORD_STRENGTH.EMPTY;
if (!password || password === "" || password.length <= 0) {
return passwordStrength;
}
if (password.length >= PASSWORD_MIN_LENGTH) {
passwordStrength = E_PASSWORD_STRENGTH.STRENGTH_NOT_VALID;
} else {
passwordStrength = E_PASSWORD_STRENGTH.LENGTH_NOT_VALID;
return passwordStrength;
}
const passwordCriteriaValidation = PASSWORD_CRITERIA.map((criteria) => criteria.isCriteriaValid(password)).every(
(criterion) => criterion
);
const passwordStrengthScore = zxcvbn(password).score;
if (passwordCriteriaValidation === false || passwordStrengthScore <= 2) {
passwordStrength = E_PASSWORD_STRENGTH.STRENGTH_NOT_VALID;
return passwordStrength;
}
if (passwordCriteriaValidation === true && passwordStrengthScore >= 3) {
passwordStrength = E_PASSWORD_STRENGTH.STRENGTH_VALID;
}
return passwordStrength;
const result = zxcvbn(password);
return result.score;
};
@@ -1,6 +1,6 @@
"use client";
import { FC, ReactNode, useEffect } from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/navigation";
// components
import { InstanceSidebar } from "@/components/admin-sidebar";
@@ -1,5 +1,5 @@
import { FC, ReactNode } from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
// components
import { LogoSpinner } from "@/components/common";
@@ -1,7 +1,7 @@
"use client";
import { FC, ReactNode, useEffect } from "react";
import { observer } from "mobx-react";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
// hooks
import { useInstance, useTheme, useUser } from "@/hooks/store";
-1
View File
@@ -1,5 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
trailingSlash: true,
reactStrictMode: false,
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "admin",
"version": "0.21.0",
"version": "0.20.0",
"private": true,
"scripts": {
"dev": "turbo run develop",
@@ -14,6 +14,7 @@
"@headlessui/react": "^1.7.19",
"@plane/types": "*",
"@plane/ui": "*",
"@plane/constants": "*",
"@tailwindcss/typography": "^0.5.9",
"@types/lodash": "^4.17.0",
"autoprefixer": "10.4.14",
@@ -22,7 +23,7 @@
"lodash": "^4.17.21",
"lucide-react": "^0.356.0",
"mobx": "^6.12.0",
"mobx-react": "^9.1.1",
"mobx-react-lite": "^4.0.5",
"next": "^14.2.3",
"next-themes": "^0.2.1",
"postcss": "^8.4.38",
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="80 80 220 220"><defs><style>.cls-1{fill:#e24329;}.cls-2{fill:#fc6d26;}.cls-3{fill:#fca326;}</style></defs><g id="LOGO"><path class="cls-1" d="M282.83,170.73l-.27-.69-26.14-68.22a6.81,6.81,0,0,0-2.69-3.24,7,7,0,0,0-8,.43,7,7,0,0,0-2.32,3.52l-17.65,54H154.29l-17.65-54A6.86,6.86,0,0,0,134.32,99a7,7,0,0,0-8-.43,6.87,6.87,0,0,0-2.69,3.24L97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82,19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91,40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/><path class="cls-2" d="M282.83,170.73l-.27-.69a88.3,88.3,0,0,0-35.15,15.8L190,229.25c19.55,14.79,36.57,27.64,36.57,27.64l40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/><path class="cls-3" d="M153.43,256.89l19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91S209.55,244,190,229.25C170.45,244,153.43,256.89,153.43,256.89Z"/><path class="cls-2" d="M132.58,185.84A88.19,88.19,0,0,0,97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82s17-12.85,36.57-27.64Z"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

@@ -1,7 +1,7 @@
// helpers
import { API_BASE_URL } from "helpers/common.helper";
// services
import { APIService } from "@/services/api.service";
import { APIService } from "services/api.service";
type TCsrfTokenResponse = {
csrf_token: string;
@@ -1,9 +1,9 @@
// helpers
import { API_BASE_URL } from "helpers/common.helper";
// services
import { APIService } from "services/api.service";
// types
import type { IUser } from "@plane/types";
// services
import { APIService } from "@/services/api.service";
interface IUserSession extends IUser {
isAuthenticated: boolean;
@@ -9,7 +9,7 @@ import {
IInstanceConfig,
} from "@plane/types";
// helpers
import { EInstanceStatus, TInstanceStatus } from "@/helpers/instance.helper";
import { EInstanceStatus, TInstanceStatus } from "@/helpers";
// services
import { InstanceService } from "@/services/instance.service";
// root store
@@ -1,4 +1,4 @@
import { enableStaticRendering } from "mobx-react";
import { enableStaticRendering } from "mobx-react-lite";
// stores
import { IInstanceStore, InstanceStore } from "./instance.store";
import { IThemeStore, ThemeStore } from "./theme.store";
@@ -1,7 +1,7 @@
import { action, observable, runInAction, makeObservable } from "mobx";
import { IUser } from "@plane/types";
// helpers
import { EUserStatus, TUserStatus } from "@/helpers/user.helper";
import { EUserStatus, TUserStatus } from "@/helpers";
// services
import { AuthService } from "@/services/auth.service";
import { UserService } from "@/services/user.service";
+1 -4
View File
@@ -7,10 +7,7 @@
"jsx": "preserve",
"esModuleInterop": true,
"paths": {
"@/*": ["core/*"],
"@/helpers/*": ["helpers/*"],
"@/public/*": ["public/*"],
"@/plane-admin/*": ["ce/*"]
"@/*": ["*"]
},
"plugins": [
{
-149
View File
@@ -1,149 +0,0 @@
# *****************************************************************************
# STAGE 1: Build the project
# *****************************************************************************
FROM node:18-alpine AS builder
RUN apk add --no-cache libc6-compat
# Set working directory
WORKDIR /app
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
RUN yarn global add turbo
COPY . .
RUN turbo prune --scope=web --scope=space --scope=admin --docker
# *****************************************************************************
# STAGE 2: Install dependencies & build the project
# *****************************************************************************
# Add lockfile and package.json's of isolated subworkspace
FROM node:18-alpine AS installer
RUN apk add --no-cache libc6-compat
WORKDIR /app
# First install the dependencies (as they change less often)
COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/yarn.lock ./yarn.lock
RUN yarn install
# # Build the project
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
ARG NEXT_PUBLIC_API_BASE_URL=""
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
ENV NEXT_TELEMETRY_DISABLED 1
ENV TURBO_TELEMETRY_DISABLED 1
RUN yarn turbo run build
# *****************************************************************************
# STAGE 3: Copy the project and start it
# *****************************************************************************
# FROM makeplane/plane-aio-base AS runner
FROM makeplane/plane-aio-base:develop AS runner
WORKDIR /app
SHELL [ "/bin/bash", "-c" ]
# PYTHON APPLICATION SETUP
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
COPY apiserver/requirements.txt ./api/
COPY apiserver/requirements ./api/requirements
RUN python3.12 -m venv /app/venv && \
source /app/venv/bin/activate && \
/app/venv/bin/pip install --upgrade pip && \
/app/venv/bin/pip install -r ./api/requirements.txt --compile --no-cache-dir
# Add in Django deps and generate Django's static files
COPY apiserver/manage.py ./api/manage.py
COPY apiserver/plane ./api/plane/
COPY apiserver/templates ./api/templates/
COPY package.json ./api/package.json
COPY apiserver/bin ./api/bin/
RUN chmod +x ./api/bin/*
RUN chmod -R 777 ./api/
# NEXTJS BUILDS
COPY --from=installer /app/web/next.config.js ./web/
COPY --from=installer /app/web/package.json ./web/
COPY --from=installer /app/web/.next/standalone ./web
COPY --from=installer /app/web/.next/static ./web/web/.next/static
COPY --from=installer /app/web/public ./web/web/public
COPY --from=installer /app/space/next.config.js ./space/
COPY --from=installer /app/space/package.json ./space/
COPY --from=installer /app/space/.next/standalone ./space
COPY --from=installer /app/space/.next/static ./space/space/.next/static
COPY --from=installer /app/space/public ./space/space/public
COPY --from=installer /app/admin/next.config.js ./admin/
COPY --from=installer /app/admin/package.json ./admin/
COPY --from=installer /app/admin/.next/standalone ./admin
COPY --from=installer /app/admin/.next/static ./admin/admin/.next/static
COPY --from=installer /app/admin/public ./admin/admin/public
ARG NEXT_PUBLIC_API_BASE_URL=""
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
ARG NEXT_PUBLIC_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ENV NEXT_TELEMETRY_DISABLED 1
ENV TURBO_TELEMETRY_DISABLED 1
COPY aio/supervisord.conf /app/supervisord.conf
COPY aio/aio.sh /app/aio.sh
RUN chmod +x /app/aio.sh
COPY aio/pg-setup.sh /app/pg-setup.sh
RUN chmod +x /app/pg-setup.sh
COPY deploy/selfhost/variables.env /app/plane.env
# NGINX Conf Copy
COPY ./aio/nginx.conf.aio /etc/nginx/nginx.conf.template
COPY ./nginx/env.sh /app/nginx-start.sh
RUN chmod +x /app/nginx-start.sh
RUN ./pg-setup.sh
VOLUME [ "/app/data/minio/uploads", "/var/lib/postgresql/data" ]
CMD ["/usr/bin/supervisord", "-c", "/app/supervisord.conf"]
-92
View File
@@ -1,92 +0,0 @@
FROM --platform=$BUILDPLATFORM tonistiigi/binfmt as binfmt
FROM debian:12-slim
# Set environment variables to non-interactive for apt
ENV DEBIAN_FRONTEND=noninteractive
SHELL [ "/bin/bash", "-c" ]
# Update the package list and install prerequisites
RUN apt-get update && \
apt-get install -y \
gnupg2 curl ca-certificates lsb-release software-properties-common \
build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev \
libsqlite3-dev wget llvm libncurses5-dev libncursesw5-dev xz-utils \
tk-dev libffi-dev liblzma-dev supervisor nginx nano vim ncdu
# Install Redis 7.2
RUN echo "deb http://deb.debian.org/debian $(lsb_release -cs)-backports main" > /etc/apt/sources.list.d/backports.list && \
curl -fsSL https://packages.redis.io/gpg | gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg && \
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" > /etc/apt/sources.list.d/redis.list && \
apt-get update && \
apt-get install -y redis-server
# Install PostgreSQL 15
ENV POSTGRES_VERSION 15
RUN curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /usr/share/keyrings/pgdg-archive-keyring.gpg && \
echo "deb [signed-by=/usr/share/keyrings/pgdg-archive-keyring.gpg] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \
apt-get update && \
apt-get install -y postgresql-$POSTGRES_VERSION postgresql-client-$POSTGRES_VERSION && \
mkdir -p /var/lib/postgresql/data && \
chown -R postgres:postgres /var/lib/postgresql
# Install MinIO
ARG TARGETARCH
RUN if [ "$TARGETARCH" = "amd64" ]; then \
curl -fSl https://dl.min.io/server/minio/release/linux-amd64/minio -o /usr/local/bin/minio; \
elif [ "$TARGETARCH" = "arm64" ]; then \
curl -fSl https://dl.min.io/server/minio/release/linux-arm64/minio -o /usr/local/bin/minio; \
else \
echo "Unsupported architecture: $TARGETARCH"; exit 1; \
fi && \
chmod +x /usr/local/bin/minio
# Install Node.js 18
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \
apt-get install -y nodejs
# Install Python 3.12 from source
RUN cd /usr/src && \
wget https://www.python.org/ftp/python/3.12.0/Python-3.12.0.tgz && \
tar xzf Python-3.12.0.tgz && \
cd Python-3.12.0 && \
./configure --enable-optimizations && \
make altinstall && \
rm -f /usr/src/Python-3.12.0.tgz
RUN python3.12 -m pip install --upgrade pip
RUN echo "alias python=/usr/local/bin/python3.12" >> ~/.bashrc && \
echo "alias pip=/usr/local/bin/pip3.12" >> ~/.bashrc
# Clean up
RUN apt-get clean && \
rm -rf /var/lib/apt/lists/* /usr/src/Python-3.12.0
WORKDIR /app
RUN mkdir -p /app/{data,logs} && \
mkdir -p /app/data/{redis,pg,minio,nginx} && \
mkdir -p /app/logs/{access,error} && \
mkdir -p /etc/supervisor/conf.d
# Create Supervisor configuration file
COPY supervisord.base /app/supervisord.conf
RUN apt-get update && \
apt-get install -y sudo lsof net-tools libpq-dev procps gettext && \
apt-get clean
RUN sudo -u postgres /usr/lib/postgresql/$POSTGRES_VERSION/bin/initdb -D /var/lib/postgresql/data
COPY postgresql.conf /etc/postgresql/postgresql.conf
RUN echo "alias python=/usr/local/bin/python3.12" >> ~/.bashrc && \
echo "alias pip=/usr/local/bin/pip3.12" >> ~/.bashrc
# Expose ports for Redis, PostgreSQL, and MinIO
EXPOSE 6379 5432 9000 80
# Start Supervisor
CMD ["/usr/bin/supervisord", "-c", "/app/supervisord.conf"]
-30
View File
@@ -1,30 +0,0 @@
#!/bin/bash
set -e
if [ "$1" = 'api' ]; then
source /app/venv/bin/activate
cd /app/api
exec ./bin/docker-entrypoint-api.sh
elif [ "$1" = 'worker' ]; then
source /app/venv/bin/activate
cd /app/api
exec ./bin/docker-entrypoint-worker.sh
elif [ "$1" = 'beat' ]; then
source /app/venv/bin/activate
cd /app/api
exec ./bin/docker-entrypoint-beat.sh
elif [ "$1" = 'migrator' ]; then
source /app/venv/bin/activate
cd /app/api
exec ./bin/docker-entrypoint-migrator.sh
elif [ "$1" = 'web' ]; then
node /app/web/web/server.js
elif [ "$1" = 'space' ]; then
node /app/space/space/server.js
elif [ "$1" = 'admin' ]; then
node /app/admin/admin/server.js
else
echo "Command not found"
exit 1
fi
-73
View File
@@ -1,73 +0,0 @@
events {
}
http {
sendfile on;
server {
listen 80;
root /www/data/;
access_log /var/log/nginx/access.log;
client_max_body_size ${FILE_SIZE_LIMIT};
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "interest-cohort=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Forwarded-Proto "${dollar}scheme";
add_header X-Forwarded-Host "${dollar}host";
add_header X-Forwarded-For "${dollar}proxy_add_x_forwarded_for";
add_header X-Real-IP "${dollar}remote_addr";
location / {
proxy_http_version 1.1;
proxy_set_header Upgrade ${dollar}http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host ${dollar}http_host;
proxy_pass http://localhost:3001/;
}
location /spaces/ {
rewrite ^/spaces/?$ /spaces/login break;
proxy_http_version 1.1;
proxy_set_header Upgrade ${dollar}http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host ${dollar}http_host;
proxy_pass http://localhost:3002/spaces/;
}
location /god-mode/ {
proxy_http_version 1.1;
proxy_set_header Upgrade ${dollar}http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host ${dollar}http_host;
proxy_pass http://localhost:3003/god-mode/;
}
location /api/ {
proxy_http_version 1.1;
proxy_set_header Upgrade ${dollar}http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host ${dollar}http_host;
proxy_pass http://localhost:8000/api/;
}
location /auth/ {
proxy_http_version 1.1;
proxy_set_header Upgrade ${dollar}http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host ${dollar}http_host;
proxy_pass http://localhost:8000/auth/;
}
location /${BUCKET_NAME}/ {
proxy_http_version 1.1;
proxy_set_header Upgrade ${dollar}http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host ${dollar}http_host;
proxy_pass http://localhost:9000/uploads/;
}
}
}
-14
View File
@@ -1,14 +0,0 @@
#!/bin/bash
# Variables
set -o allexport
source plane.env set
set +o allexport
export PGHOST=localhost
sudo -u postgres "/usr/lib/postgresql/${POSTGRES_VERSION}/bin/pg_ctl" -D /var/lib/postgresql/data start
sudo -u postgres "/usr/lib/postgresql/${POSTGRES_VERSION}/bin/psql" --command "CREATE USER $POSTGRES_USER WITH SUPERUSER PASSWORD '$POSTGRES_PASSWORD';" && \
sudo -u postgres "/usr/lib/postgresql/${POSTGRES_VERSION}/bin/createdb" -O "$POSTGRES_USER" "$POSTGRES_DB" && \
sudo -u postgres "/usr/lib/postgresql/${POSTGRES_VERSION}/bin/pg_ctl" -D /var/lib/postgresql/data stop
-12
View File
@@ -1,12 +0,0 @@
# PostgreSQL configuration file
# Allow connections from any IP address
listen_addresses = '*'
# Set the maximum number of connections
max_connections = 100
# Set the shared buffers size
shared_buffers = 128MB
# Other custom configurations can be added here
-37
View File
@@ -1,37 +0,0 @@
[supervisord]
user=root
nodaemon=true
stderr_logfile=/app/logs/error/supervisor.err.log
stdout_logfile=/app/logs/access/supervisor.out.log
[program:redis]
directory=/app/data/redis
command=redis-server
autostart=true
autorestart=true
stderr_logfile=/app/logs/error/redis.err.log
stdout_logfile=/app/logs/access/redis.out.log
[program:postgresql]
user=postgres
command=/usr/lib/postgresql/15/bin/postgres --config-file=/etc/postgresql/15/main/postgresql.conf
autostart=true
autorestart=true
stderr_logfile=/app/logs/error/postgresql.err.log
stdout_logfile=/app/logs/access/postgresql.out.log
[program:minio]
directory=/app/data/minio
command=minio server /app/data/minio
autostart=true
autorestart=true
stderr_logfile=/app/logs/error/minio.err.log
stdout_logfile=/app/logs/access/minio.out.log
[program:nginx]
directory=/app/data/nginx
command=/usr/sbin/nginx -g 'daemon off;'
autostart=true
autorestart=true
stderr_logfile=/app/logs/error/nginx.err.log
stdout_logfile=/app/logs/access/nginx.out.log
-115
View File
@@ -1,115 +0,0 @@
[supervisord]
user=root
nodaemon=true
priority=1
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stdout
stderr_logfile_maxbytes=0
[program:redis]
directory=/app/data/redis
command=redis-server
autostart=true
autorestart=true
priority=1
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stdout
stderr_logfile_maxbytes=0
[program:postgresql]
user=postgres
command=/usr/lib/postgresql/15/bin/postgres -D /var/lib/postgresql/data --config-file=/etc/postgresql/postgresql.conf
autostart=true
autorestart=true
priority=1
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stdout
stderr_logfile_maxbytes=0
[program:minio]
directory=/app/data/minio
command=minio server /app/data/minio
autostart=true
autorestart=true
priority=1
stdout_logfile=/app/logs/access/minio.log
stderr_logfile=/app/logs/error/minio.err.log
[program:nginx]
command=/app/nginx-start.sh
autostart=true
autorestart=true
priority=1
stdout_logfile=/app/logs/access/nginx.log
stderr_logfile=/app/logs/error/nginx.err.log
[program:web]
command=/app/aio.sh web
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stdout
stderr_logfile_maxbytes=0
environment=PORT=3001,HOSTNAME=0.0.0.0
[program:space]
command=/app/aio.sh space
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stdout
stderr_logfile_maxbytes=0
environment=PORT=3002,HOSTNAME=0.0.0.0
[program:admin]
command=/app/aio.sh admin
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stdout
stderr_logfile_maxbytes=0
environment=PORT=3003,HOSTNAME=0.0.0.0
[program:migrator]
command=/app/aio.sh migrator
autostart=true
autorestart=false
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stdout
stderr_logfile_maxbytes=0
[program:api]
command=/app/aio.sh api
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stdout
stderr_logfile_maxbytes=0
[program:worker]
command=/app/aio.sh worker
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stdout
stderr_logfile_maxbytes=0
[program:beat]
command=/app/aio.sh beat
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stdout
stderr_logfile_maxbytes=0
View File
View File
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "plane-api",
"version": "0.21.0"
"version": "0.20.0"
}
+4 -31
View File
@@ -21,7 +21,6 @@ from plane.db.models import (
ProjectMember,
State,
User,
Project,
)
from .base import BaseSerializer
@@ -29,8 +28,6 @@ from .cycle import CycleLiteSerializer, CycleSerializer
from .module import ModuleLiteSerializer, ModuleSerializer
from .state import StateLiteSerializer
from .user import UserLiteSerializer
from plane.utils.metadata import get_metadata
from plane.utils.presigned_url_generator import generate_download_presigned_url
class IssueSerializer(BaseSerializer):
@@ -275,8 +272,6 @@ class LabelSerializer(BaseSerializer):
class IssueLinkSerializer(BaseSerializer):
metadata = serializers.SerializerMethodField()
class Meta:
model = IssueLink
fields = "__all__"
@@ -291,12 +286,6 @@ class IssueLinkSerializer(BaseSerializer):
"updated_at",
]
def get_metadata(self, obj):
logo = obj.metadata.get("logo", None)
if logo:
obj.metadata["logo"] = generate_download_presigned_url(logo)
return obj.metadata
def validate_url(self, value):
# Check URL format
validate_url = URLValidator()
@@ -320,33 +309,17 @@ class IssueLinkSerializer(BaseSerializer):
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
)
# Workspace
project = Project.objects.get(pk=validated_data.get("project_id"))
# Fetch metadata from URL
validated_data["metadata"] = get_metadata(
validated_data.get("url"), project.workspace_id
)
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"}
)
validated_data["metadata"] = get_metadata(
validated_data.get("url"), instance.workspace_id
)
return super().update(instance, validated_data)

Some files were not shown because too many files have changed in this diff Show More