fix: merge conflicts resolved

This commit is contained in:
sriram veeraghanta
2024-01-23 18:15:20 +05:30
12 changed files with 417 additions and 16 deletions

View File

@@ -0,0 +1,28 @@
#!/bin/bash
set -e
python manage.py wait_for_db
python manage.py migrate
# Create the default bucket
#!/bin/bash
# Collect system information
HOSTNAME=$(hostname)
MAC_ADDRESS=$(ip link show | awk '/ether/ {print $2}' | head -n 1)
CPU_INFO=$(cat /proc/cpuinfo)
MEMORY_INFO=$(free -h)
DISK_INFO=$(df -h)
# Concatenate information and compute SHA-256 hash
SIGNATURE=$(echo "$HOSTNAME$MAC_ADDRESS$CPU_INFO$MEMORY_INFO$DISK_INFO" | sha256sum | awk '{print $1}')
# Export the variables
export MACHINE_SIGNATURE=$SIGNATURE
# Register instance
python manage.py setup_instance $INSTANCE_ADMIN_EMAIL
# Create the default bucket
python manage.py create_bucket
exec gunicorn -w $GUNICORN_WORKERS -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:${PORT:-8000} --max-requests 1200 --max-requests-jitter 1000 --access-logfile -

View File

@@ -25,6 +25,7 @@ from plane.db.models import (
User,
IssueProperty,
)
from plane.bgtasks.user_welcome_task import send_welcome_slack
@shared_task
@@ -54,6 +55,15 @@ def service_importer(service, importer_id):
ignore_conflicts=True,
)
_ = [
send_welcome_slack.delay(
str(user.id),
True,
f"{user.email} was imported to Plane from {service}",
)
for user in new_users
]
workspace_users = User.objects.filter(
email__in=[
user.get("email").strip().lower()

View File

@@ -0,0 +1,36 @@
# Django imports
from django.conf import settings
# Third party imports
from celery import shared_task
from sentry_sdk import capture_exception
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
# Module imports
from plane.db.models import User
@shared_task
def send_welcome_slack(user_id, created, message):
try:
instance = User.objects.get(pk=user_id)
if created and not instance.is_bot:
# Send message on slack as well
if settings.SLACK_BOT_TOKEN:
client = WebClient(token=settings.SLACK_BOT_TOKEN)
try:
_ = client.chat_postMessage(
channel="#trackers",
text=message,
)
except SlackApiError as e:
print(f"Got an error: {e.response['error']}")
return
except Exception as e:
# Print logs if in DEBUG mode
if settings.DEBUG:
print(e)
capture_exception(e)
return

View File

@@ -20,6 +20,18 @@ from plane.db.models import Workspace, WorkspaceMemberInvite, User
from plane.license.utils.instance_value import get_email_configuration
def push_updated_to_slack(workspace, workspace_member_invite):
# Send message on slack as well
client = WebClient(token=settings.SLACK_BOT_TOKEN)
try:
_ = client.chat_postMessage(
channel="#trackers",
text=f"{workspace_member_invite.email} has been invited to {workspace.name} as a {workspace_member_invite.role}",
)
except SlackApiError as e:
print(f"Got an error: {e.response['error']}")
@shared_task
def workspace_invitation(email, workspace_id, token, current_site, invitor):
try:
@@ -82,6 +94,10 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
msg.attach_alternative(html_content, "text/html")
msg.send()
# Send message on slack as well
if settings.SLACK_BOT_TOKEN:
push_updated_to_slack(workspace, workspace_member_invite)
return
except (Workspace.DoesNotExist, WorkspaceMemberInvite.DoesNotExist) as e:
print("Workspace or WorkspaceMember Invite Does not exists")

View File

@@ -12,7 +12,14 @@ from django.contrib.auth.models import (
PermissionsMixin,
)
from django.utils import timezone
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.conf import settings
# Third party imports
from sentry_sdk import capture_exception
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
def get_default_onboarding():
return {
@@ -134,3 +141,23 @@ class User(AbstractBaseUser, PermissionsMixin):
self.is_staff = True
super(User, self).save(*args, **kwargs)
@receiver(post_save, sender=User)
def send_welcome_slack(sender, instance, created, **kwargs):
try:
if created and not instance.is_bot:
# Send message on slack as well
if settings.SLACK_BOT_TOKEN:
client = WebClient(token=settings.SLACK_BOT_TOKEN)
try:
_ = client.chat_postMessage(
channel="#trackers",
text=f"New user {instance.email} has signed up and begun the onboarding journey.",
)
except SlackApiError as e:
print(f"Got an error: {e.response['error']}")
return
except Exception as e:
capture_exception(e)
return

View File

@@ -0,0 +1,74 @@
# Python imports
import json
import secrets
import uuid
# Django imports
from django.contrib.auth.hashers import make_password
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
# Module imports
from plane.license.models import Instance, InstanceAdmin
from plane.db.models import User
class Command(BaseCommand):
help = "Check if instance in registered else register"
def add_arguments(self, parser):
# Positional argument
parser.add_argument("admin_email", type=str, help="Admin Email")
def handle(self, *args, **options):
with open("package.json", "r") as file:
# Load JSON content from the file
data = json.load(file)
admin_email = options.get("admin_email", False)
if not admin_email:
raise CommandError("admin email is required")
user_count = User.objects.filter(is_bot=False).count()
user = User.objects.filter(email=admin_email).first()
if user is None:
user = User.objects.create(
email=admin_email,
username=uuid.uuid4().hex,
password=make_password(uuid.uuid4().hex),
)
try:
# Check if the instance is registered
instance = Instance.objects.first()
if instance is None:
instance = Instance.objects.create(
instance_name="Plane Enterprise",
instance_id=secrets.token_hex(12),
license_key=None,
api_key=secrets.token_hex(8),
version=data.get("version"),
last_checked_at=timezone.now(),
user_count=user_count,
is_verified=True,
is_setup_done=True,
is_signup_screen_visited=True,
)
# Get or create an instance admin
_, created = InstanceAdmin.objects.get_or_create(
user=user, instance=instance, role=20, is_verified=True
)
if not created:
self.stdout.write(
self.style.WARNING("given email is already an instance admin")
)
self.stdout.write(self.style.SUCCESS(f"Successful"))
except Exception as e:
print(e)
raise CommandError("Failure")

View File

@@ -314,7 +314,7 @@ if bool(os.environ.get("SENTRY_DSN", False)) and os.environ.get(
# Application Envs
PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False) # For External
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN", False)
FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880))
# Unsplash Access key

View File

@@ -17,17 +17,14 @@
"NEXT_PUBLIC_POSTHOG_KEY",
"NEXT_PUBLIC_POSTHOG_HOST",
"JITSU_TRACKER_ACCESS_KEY",
"JITSU_TRACKER_HOST"
"JITSU_TRACKER_HOST",
"NEXT_PUBLIC_PRO_PLAN_MONTHLY_REDIRECT_URL",
"NEXT_PUBLIC_PRO_PLAN_YEARLY_REDIRECT_URL"
],
"pipeline": {
"build": {
"dependsOn": [
"^build"
],
"outputs": [
".next/**",
"dist/**"
]
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
},
"develop": {
"cache": false,
@@ -44,9 +41,7 @@
]
},
"test": {
"dependsOn": [
"^build"
],
"dependsOn": ["^build"],
"outputs": []
},
"lint": {

View File

@@ -0,0 +1 @@
export * from "./pro-plan-modal";

View File

@@ -0,0 +1,190 @@
import { FC, Fragment, useState } from "react";
import { Dialog, Transition, Tab } from "@headlessui/react";
import { CheckCircle } from "lucide-react";
import { useMobxStore } from "lib/mobx/store-provider";
function classNames(...classes: any[]) {
return classes.filter(Boolean).join(" ");
}
const PRICING_CATEGORIES = ["Monthly", "Yearly"];
const MONTHLY_PLAN_ITEMS = [
"White-glove onboarding for your use-cases",
"Bespoke implementation",
"Priority integrations",
"Priority Support and SLAs",
"Early access to all paid features",
"Locked-in discount for a whole year",
];
const YEARLY_PLAN_ITEMS = [
"White-glove onboarding for your use-cases",
"Bespoke implementation",
"Priority integrations",
"Priority Support and SLAs",
"Early access to all paid features",
"Tiered discounts for the second and third years",
];
export type ProPlanModalProps = {
isOpen: boolean;
handleClose: () => void;
};
export const ProPlanModal: FC<ProPlanModalProps> = (props) => {
const { isOpen, handleClose } = props;
// store
const {
trackEvent: { captureEvent },
} = useMobxStore();
// states
const [tabIndex, setTabIndex] = useState(0);
const handleProPlaneMonthRedirection = () => {
if (process.env.NEXT_PUBLIC_PRO_PLAN_MONTHLY_REDIRECT_URL) {
window.open(process.env.NEXT_PUBLIC_PRO_PLAN_MONTHLY_REDIRECT_URL, "_blank");
captureEvent("pro_plan_modal_month_redirection");
}
};
const handleProPlanYearlyRedirection = () => {
if (process.env.NEXT_PUBLIC_PRO_PLAN_YEARLY_REDIRECT_URL) {
window.open(process.env.NEXT_PUBLIC_PRO_PLAN_YEARLY_REDIRECT_URL, "_blank");
captureEvent("pro_plan_modal_yearly_redirection");
}
};
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-50" onClose={handleClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-custom-backdrop" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-lg transform overflow-hidden rounded-2xl bg-custom-background-100 p-6 text-left align-middle shadow-xl transition-all border-[0.5px] border-custom-border-100">
<Dialog.Title as="h2" className="text-2xl font-bold leading-6 mt-4 flex justify-center items-center">
Early-adopter pricing for believers
</Dialog.Title>
<div className="mt-2 mb-5">
<p className="text-center text-sm mb-6 px-10 text-custom-text-200">
Build Plane to your specs. You decide what we prioritize and build for everyone. Also get tailored
onboarding + implementation and priority support.
</p>
<Tab.Group>
<div className="flex w-full justify-center">
<Tab.List className="flex space-x-1 rounded-xl bg-custom-background-80 p-1 w-[72%]">
{PRICING_CATEGORIES.map((category, index) => (
<Tab
key={category}
className={({ selected }) =>
classNames(
"w-full rounded-lg py-2 text-sm font-medium leading-5",
"ring-white/60 ring-offset-2 ring-offset-custom-primary-90 focus:outline-none",
selected
? "bg-custom-background-100 text-custom-primary-100 shadow"
: "hover:bg-custom-background-80 text-custom-text-300 hover:text-custom-text-100"
)
}
onClick={() => setTabIndex(index)}
>
<>
{category}
{category === "Yearly" && (
<span className="bg-custom-primary-100 text-white rounded-full px-2 py-1 ml-1 text-xs">
-28%
</span>
)}
</>
</Tab>
))}
</Tab.List>
</div>
<Tab.Panels className="mt-2">
<Tab.Panel className={classNames("rounded-xl bg-custom-background-100 p-3")}>
<p className="ml-4 text-4xl font-bold mb-2">
$7
<span className="text-sm ml-3 text-custom-text-300">/user/month</span>
</p>
<ul>
{MONTHLY_PLAN_ITEMS.map((item) => (
<li key={item} className="relative rounded-md p-3 flex">
<p className="text-sm font-medium leading-5 flex items-center">
<CheckCircle className="h-4 w-4 mr-4" />
<span>{item}</span>
</p>
</li>
))}
</ul>
<div className="flex justify-center w-full">
<div className="relative inline-flex group mt-8">
<div className="absolute transition-all duration-1000 opacity-50 -inset-px bg-gradient-to-r from-[#44BCFF] via-[#FF44EC] to-[#FF675E] rounded-xl blur-lg group-hover:opacity-100 group-hover:-inset-1 group-hover:duration-200 animate-tilt" />
<button
type="button"
className="relative inline-flex items-center justify-center px-8 py-4 text-sm font-medium border-custom-border-100 border-[1.5px] transition-all duration-200 bg-custom-background-100 rounded-xl focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-custom-border-200"
onClick={handleProPlaneMonthRedirection}
>
Become Early Adopter
</button>
</div>
</div>
</Tab.Panel>
<Tab.Panel className={classNames("rounded-xl bg-custom-background-100 p-3")}>
<p className="ml-4 text-4xl font-bold mb-2">
$5
<span className="text-sm ml-3 text-custom-text-300">/user/month</span>
</p>
<ul>
{YEARLY_PLAN_ITEMS.map((item) => (
<li key={item} className="relative rounded-md p-3 flex">
<p className="text-sm font-medium leading-5 flex items-center">
<CheckCircle className="h-4 w-4 mr-4" />
<span>{item}</span>
</p>
</li>
))}
</ul>
<div className="flex justify-center w-full">
<div className="relative inline-flex group mt-8">
<div className="absolute transition-all duration-1000 opacity-50 -inset-px bg-gradient-to-r from-[#44BCFF] via-[#FF44EC] to-[#FF675E] rounded-xl blur-lg group-hover:opacity-100 group-hover:-inset-1 group-hover:duration-200 animate-tilt" />
<button
type="button"
className="relative inline-flex items-center justify-center px-8 py-4 text-sm font-medium border-custom-border-100 border-[1.5px] transition-all duration-200 bg-custom-background-100 rounded-xl focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-custom-border-200"
onClick={handleProPlanYearlyRedirection}
>
Become Early Adopter
</button>
</div>
</div>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
);
};

View File

@@ -7,9 +7,11 @@ import { useApplication } from "hooks/store";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// icons
import { FileText, HelpCircle, MessagesSquare, MoveLeft, Zap } from "lucide-react";
import { DiscordIcon, GithubIcon } from "@plane/ui";
import { DiscordIcon, GithubIcon, Button } from "@plane/ui";
// assets
import packageJson from "package.json";
// components
import { ProPlanModal } from "components/license";
const helpOptions = [
{
@@ -40,10 +42,13 @@ export interface WorkspaceHelpSectionProps {
}
export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(() => {
// states
const [isProPlanModalOpen, setIsProPlanModalOpen] = useState(false);
// store hooks
const {
theme: { sidebarCollapsed, toggleSidebar },
commandPalette: { toggleShortcutModal },
eventTracker: { captureEvent },
} = useApplication();
// states
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
@@ -54,17 +59,27 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
const isCollapsed = sidebarCollapsed || false;
const handleProPlanModalOpen = () => {
setIsProPlanModalOpen(true);
captureEvent("pro_plan_modal_opened");
};
return (
<>
<ProPlanModal isOpen={isProPlanModalOpen} handleClose={() => setIsProPlanModalOpen(false)} />
<div
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 px-4 py-2 ${
isCollapsed ? "flex-col" : ""
}`}
>
{!isCollapsed && (
<div className="w-1/2 cursor-default rounded-md bg-green-500/10 px-2.5 py-1.5 text-center text-sm font-medium text-green-500 outline-none">
Free Plan
</div>
<Button
variant="outline-primary"
className="w-1/2 cursor-pointer rounded-2xl px-2.5 py-1.5 text-center text-sm font-medium outline-none"
onClick={handleProPlanModalOpen}
>
Plane Pro
</Button>
)}
<div className={`flex items-center gap-1 ${isCollapsed ? "flex-col justify-center" : "w-1/2 justify-evenly"}`}>
<button

View File

@@ -11,6 +11,7 @@ export interface IEventTrackerStore {
payload: object | [] | null,
group?: { isGrouping: boolean | null; groupType: string | null; groupId: string | null } | null
) => void;
captureEvent: (eventName: string, payload?: any) => void;
}
export class EventTrackerStore implements IEventTrackerStore {
@@ -76,4 +77,12 @@ export class EventTrackerStore implements IEventTrackerStore {
}
this.setTrackElement("");
};
captureEvent = (eventName: string, payload?: any) => {
try {
posthog?.capture(eventName, payload);
} catch (error) {
console.log(error);
}
};
}