Compare commits

...

10 Commits

Author SHA1 Message Date
pablohashescobar 4cc1b79d81 chore: instance tracing 2024-10-04 21:35:13 +05:30
sriram veeraghanta 4a6f646317 fix: lockfile update 2024-10-04 19:38:19 +05:30
sriram veeraghanta b8e21d92bf Merge branch 'preview' of github.com:makeplane/plane into preview 2024-10-04 19:26:06 +05:30
sriram veeraghanta b87d5c5be6 fix: version upgrade 2024-10-04 19:25:49 +05:30
ach5948 ceda06e88d fix: Remove typo from Contributing doc (#5736) 2024-10-04 19:24:47 +05:30
sriram veeraghanta eb344881c2 Merge branch 'preview' of github.com:makeplane/plane into develop 2024-10-04 19:22:26 +05:30
Satish Gandham 01257a6936 chore: permission layer and updated issues v1 query from workspace to project level (#5753)
Co-authored-by: gurusainath <gurusainath007@gmail.com>
2024-10-04 18:34:46 +05:30
Prateek Shourya 51b01ebcac [WEB-2580] chore: improvements for custom search select. (#5744)
* [WEB-2580] chore: improvements for custom search select.

* chore: update optionTooltip prop.

* chore: update option tooltip prop.

* chore: minor updates.
2024-10-04 17:31:09 +05:30
sriram veeraghanta 0a8d66dcc3 fix: trace information setup 2024-10-04 16:40:33 +05:30
sriram veeraghanta a5e3e4fe7d fix: api tracing 2024-10-04 01:14:29 +05:30
28 changed files with 1933 additions and 1533 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ Thank you for showing an interest in contributing to Plane! All kinds of contrib
## Submitting an issue
Before submitting a new issue, please search the [issues](https://github.com/makeplane/plane/issues) tab. Maybe an issue or discussion already exists and might inform you of workarounds. Otherwise, you can give new informplaneation.
Before submitting a new issue, please search the [issues](https://github.com/makeplane/plane/issues) tab. Maybe an issue or discussion already exists and might inform you of workarounds. Otherwise, you can give new information.
While we want to fix all the [issues](https://github.com/makeplane/plane/issues), before fixing a bug we need to be able to reproduce and confirm it. Please provide us with a minimal reproduction scenario using a repository or [Gist](https://gist.github.com/). Having a live, reproducible scenario gives us the information without asking questions back & forth with additional questions like:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "admin",
"version": "0.22.0",
"version": "0.23.0",
"private": true,
"scripts": {
"dev": "turbo run develop",
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "plane-api",
"version": "0.22.0"
"version": "0.23.0"
}
+1 -1
View File
@@ -42,7 +42,7 @@ urlpatterns = [
),
# updated v2 paginated issues
path(
"workspaces/<str:slug>/v2/issues/",
"workspaces/<str:slug>/projects/<uuid:project_id>/v2/issues/",
IssuePaginatedViewSet.as_view({"get": "list"}),
name="project-issues-paginated",
),
+20 -13
View File
@@ -741,17 +741,12 @@ class DeletedIssuesListViewSet(BaseAPIView):
class IssuePaginatedViewSet(BaseViewSet):
def get_queryset(self):
workspace_slug = self.kwargs.get("slug")
# getting the project_id from the request params
project_id = self.request.GET.get("project_id", None)
project_id = self.kwargs.get("project_id")
issue_queryset = Issue.issue_objects.filter(
workspace__slug=workspace_slug
workspace__slug=workspace_slug, project_id=project_id
)
if project_id:
issue_queryset = issue_queryset.filter(project_id=project_id)
return (
issue_queryset.select_related(
"workspace", "project", "state", "parent"
@@ -793,8 +788,8 @@ class IssuePaginatedViewSet(BaseViewSet):
return paginated_data
def list(self, request, slug):
project_id = self.request.GET.get("project_id", None)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def list(self, request, slug, project_id):
cursor = request.GET.get("cursor", None)
is_description_required = request.GET.get("description", False)
updated_at = request.GET.get("updated_at__gt", None)
@@ -833,14 +828,26 @@ class IssuePaginatedViewSet(BaseViewSet):
required_fields.append("description_html")
# querying issues
base_queryset = Issue.issue_objects.filter(workspace__slug=slug)
if project_id:
base_queryset = base_queryset.filter(project_id=project_id)
base_queryset = Issue.issue_objects.filter(
workspace__slug=slug, project_id=project_id
)
base_queryset = base_queryset.order_by("updated_at")
queryset = self.get_queryset().order_by("updated_at")
# validation for guest user
project = Project.objects.get(pk=project_id, workspace__slug=slug)
project_member = ProjectMember.objects.filter(
workspace__slug=slug,
project_id=project_id,
member=request.user,
role=5,
is_active=True,
)
if project_member.exists() and not project.guest_view_all_features:
base_queryset = base_queryset.filter(created_by=request.user)
queryset = queryset.filter(created_by=request.user)
# filtering issues by greater then updated_at given by the user
if updated_at:
base_queryset = base_queryset.filter(updated_at__gt=updated_at)
@@ -74,7 +74,6 @@ class WorkSpaceMemberViewSet(BaseViewSet):
# Get all active workspace members
workspace_members = self.get_queryset()
if workspace_member.role > 5:
serializer = WorkspaceMemberAdminSerializer(
workspace_members,
+96
View File
@@ -0,0 +1,96 @@
# Third party imports
from celery import shared_task
from opentelemetry import trace
# Module imports
from plane.license.models import Instance
from plane.db.models import (
User,
Workspace,
Project,
Issue,
Module,
Cycle,
CycleIssue,
ModuleIssue,
Page,
WorkspaceMember,
)
@shared_task
def instance_traces():
# Get the tracer
tracer = trace.get_tracer(__name__)
# Check if the instance is registered
instance = Instance.objects.first()
# If instance is None then return
if instance is None:
return
if instance.is_telemetry_enabled:
# Instance details
with tracer.start_as_current_span("instance_details") as span:
# Count of all models
workspace_count = Workspace.objects.count()
user_count = User.objects.count()
project_count = Project.objects.count()
issue_count = Issue.objects.count()
module_count = Module.objects.count()
cycle_count = Cycle.objects.count()
cycle_issue_count = CycleIssue.objects.count()
module_issue_count = ModuleIssue.objects.count()
page_count = Page.objects.count()
# Set span attributes
span.set_attribute("instance_id", instance.instance_id)
span.set_attribute("instance_name", instance.instance_name)
span.set_attribute("current_version", instance.current_version)
span.set_attribute("latest_version", instance.latest_version)
span.set_attribute(
"is_telemetry_enabled", instance.is_telemetry_enabled
)
span.set_attribute("user_count", user_count)
span.set_attribute("workspace_count", workspace_count)
span.set_attribute("project_count", project_count)
span.set_attribute("issue_count", issue_count)
span.set_attribute("module_count", module_count)
span.set_attribute("cycle_count", cycle_count)
span.set_attribute("cycle_issue_count", cycle_issue_count)
span.set_attribute("module_issue_count", module_issue_count)
span.set_attribute("page_count", page_count)
# Workspace details
for workspace in Workspace.objects.all():
# Count of all models
project_count = Project.objects.filter(workspace=workspace).count()
issue_count = Issue.objects.filter(workspace=workspace).count()
module_count = Module.objects.filter(workspace=workspace).count()
cycle_count = Cycle.objects.filter(workspace=workspace).count()
cycle_issue_count = CycleIssue.objects.filter(
workspace=workspace
).count()
module_issue_count = ModuleIssue.objects.filter(
workspace=workspace
).count()
page_count = Page.objects.filter(workspace=workspace).count()
member_count = WorkspaceMember.objects.filter(
workspace=workspace
).count()
# Set span attributes
with tracer.start_as_current_span("workspace_details") as span:
span.set_attribute("workspace_id", str(workspace.id))
span.set_attribute("workspace_slug", workspace.slug)
span.set_attribute("project_count", project_count)
span.set_attribute("issue_count", issue_count)
span.set_attribute("module_count", module_count)
span.set_attribute("cycle_count", cycle_count)
span.set_attribute("cycle_issue_count", cycle_issue_count)
span.set_attribute("module_issue_count", module_issue_count)
span.set_attribute("page_count", page_count)
span.set_attribute("member_count", member_count)
return
@@ -9,7 +9,10 @@ from django.conf import settings
# Module imports
from plane.license.models import Instance
from plane.db.models import User
from plane.db.models import (
User,
)
from plane.license.bgtasks.tracer import instance_traces
class Command(BaseCommand):
@@ -21,16 +24,24 @@ class Command(BaseCommand):
"machine_signature", type=str, help="Machine signature"
)
def read_package_json(self):
with open("package.json", "r") as file:
# Load JSON content from the file
data = json.load(file)
payload = {
"instance_key": settings.INSTANCE_KEY,
"version": data.get("version", 0.1),
"user_count": User.objects.filter(is_bot=False).count(),
}
return payload
def handle(self, *args, **options):
# Check if the instance is registered
instance = Instance.objects.first()
# If instance is None then register this instance
if instance is None:
with open("package.json", "r") as file:
# Load JSON content from the file
data = json.load(file)
machine_signature = options.get(
"machine_signature", "machine-signature"
)
@@ -38,12 +49,7 @@ class Command(BaseCommand):
if not machine_signature:
raise CommandError("Machine signature is required")
payload = {
"instance_key": settings.INSTANCE_KEY,
"version": data.get("version", 0.1),
"machine_signature": machine_signature,
"user_count": User.objects.filter(is_bot=False).count(),
}
payload = self.read_package_json()
instance = Instance.objects.create(
instance_name="Plane Community Edition",
@@ -60,4 +66,15 @@ class Command(BaseCommand):
self.stdout.write(
self.style.SUCCESS("Instance already registered")
)
return
payload = self.read_package_json()
# Update the instance details
instance.last_checked_at = timezone.now()
instance.user_count = payload.get("user_count", 0)
instance.current_version = payload.get("version")
instance.latest_version = payload.get("version")
instance.save()
# Call the instance traces task
instance_traces.delay()
return
+25
View File
@@ -16,6 +16,17 @@ from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.redis import RedisIntegration
from corsheaders.defaults import default_headers
# OpenTelemetry
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
OTLPSpanExporter,
)
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.resources import Resource
from opentelemetry.instrumentation.django import DjangoInstrumentor
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Secret Key
@@ -24,6 +35,19 @@ SECRET_KEY = os.environ.get("SECRET_KEY", get_random_secret_key())
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = int(os.environ.get("DEBUG", "0"))
# Initialize Django instrumentation
DjangoInstrumentor().instrument()
# Configure the tracer provider
service_name = os.environ.get("SERVICE_NAME", "plane-ce-api")
resource = Resource.create({"service.name": service_name})
trace.set_tracer_provider(TracerProvider(resource=resource))
# Configure the OTLP exporter
otel_endpoint = os.environ.get("OTLP_ENDPOINT", "https://telemetry.plane.so")
otlp_exporter = OTLPSpanExporter(endpoint=otel_endpoint)
span_processor = BatchSpanProcessor(otlp_exporter)
trace.get_tracer_provider().add_span_processor(span_processor)
# Allowed Hosts
ALLOWED_HOSTS = ["*"]
@@ -279,6 +303,7 @@ CELERY_IMPORTS = (
"plane.bgtasks.file_asset_task",
"plane.bgtasks.email_notification_task",
"plane.bgtasks.api_logs_task",
"plane.license.bgtasks.tracer",
# management tasks
"plane.bgtasks.dummy_data_task",
)
+5
View File
@@ -62,3 +62,8 @@ zxcvbn==4.4.28
pytz==2024.1
# jwt
PyJWT==2.8.0
# OpenTelemetry
opentelemetry-api==1.27.0
opentelemetry-sdk==1.27.0
opentelemetry-instrumentation-django==0.48b0
opentelemetry-exporter-otlp==1.27.0
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "live",
"version": "0.22.0",
"version": "0.23.0",
"description": "",
"main": "./src/server.ts",
"private": true,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"repository": "https://github.com/makeplane/plane.git",
"version": "0.22.0",
"version": "0.23.0",
"license": "AGPL-3.0",
"private": true,
"workspaces": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/constants",
"version": "0.22.0",
"version": "0.23.0",
"private": true,
"main": "./index.ts"
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/editor",
"version": "0.22.0",
"version": "0.23.0",
"description": "Core Editor that powers Plane",
"private": true,
"main": "./dist/index.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@plane/eslint-config",
"private": true,
"version": "0.22.0",
"version": "0.23.0",
"files": [
"library.js",
"next.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/helpers",
"version": "0.22.0",
"version": "0.23.0",
"description": "Helper functions shared across multiple apps internally",
"private": true,
"main": "./dist/index.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tailwind-config-custom",
"version": "0.22.0",
"version": "0.23.0",
"description": "common tailwind configuration across monorepo",
"main": "index.js",
"private": true,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/types",
"version": "0.22.0",
"version": "0.23.0",
"private": true,
"types": "./src/index.d.ts",
"main": "./src/index.d.ts"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/typescript-config",
"version": "0.22.0",
"version": "0.23.0",
"private": true,
"files": [
"base.json",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@plane/ui",
"description": "UI components shared across multiple apps internally",
"private": true,
"version": "0.22.0",
"version": "0.23.0",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
@@ -1,7 +1,7 @@
import React, { useRef, useState } from "react";
import { usePopper } from "react-popper";
import { Combobox } from "@headlessui/react";
import { Check, ChevronDown, Search } from "lucide-react";
import { Check, ChevronDown, Info, Search } from "lucide-react";
import { createPortal } from "react-dom";
// plane helpers
import { useOutsideClickDetector } from "@plane/helpers";
@@ -11,6 +11,8 @@ import { useDropdownKeyDown } from "../hooks/use-dropdown-key-down";
import { cn } from "../../helpers";
// types
import { ICustomSearchSelectProps } from "./helper";
// local components
import { Tooltip } from "../tooltip";
export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
const {
@@ -95,11 +97,10 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
<button
ref={setReferenceElement}
type="button"
className={`flex w-full items-center justify-between gap-1 text-xs ${
disabled
? "cursor-not-allowed text-custom-text-200"
: "cursor-pointer hover:bg-custom-background-80"
} ${customButtonClassName}`}
className={`flex w-full items-center justify-between gap-1 text-xs ${disabled
? "cursor-not-allowed text-custom-text-200"
: "cursor-pointer hover:bg-custom-background-80"
} ${customButtonClassName}`}
onClick={toggleDropdown}
>
{customButton}
@@ -170,17 +171,32 @@ export const CustomSearchSelect = (props: ICustomSearchSelectProps) => {
"w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none",
{
"bg-custom-background-80": active,
"text-custom-text-400 opacity-60 cursor-not-allowed": option.disabled,
}
)
}
onClick={() => {
if (!multiple) closeDropdown();
}}
disabled={option.disabled}
>
{({ selected }) => (
<>
<span className="flex-grow truncate">{option.content}</span>
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
{option.tooltip && (
<>
{
typeof option.tooltip === "string" ? (
<Tooltip tooltipContent={option.tooltip}>
<Info className="h-3.5 w-3.5 flex-shrink-0 cursor-pointer text-custom-text-200" />
</Tooltip>
) : (
option.tooltip
)
}
</>
)}
</>
)}
</Combobox.Option>
+8 -6
View File
@@ -44,12 +44,14 @@ interface CustomSearchSelectProps {
onChange: any;
onClose?: () => void;
options:
| {
value: any;
query: string;
content: React.ReactNode;
}[]
| undefined;
| {
value: any;
query: string;
content: React.ReactNode;
disabled?: boolean;
tooltip?: string | React.ReactNode;
}[]
| undefined;
}
interface SingleValueProps {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "space",
"version": "0.22.0",
"version": "0.23.0",
"private": true,
"scripts": {
"dev": "turbo run develop",
@@ -1,12 +1,23 @@
import { Control } from "react-hook-form";
// types
import { TIssue } from "@plane/types";
import { TBulkIssueProperties, TIssue } from "@plane/types";
type TIssueTypeSelectProps = {
control: Control<TIssue>;
export type TIssueFields = TIssue & TBulkIssueProperties;
export type TIssueTypeDropdownVariant = "xs" | "sm";
export type TIssueTypeSelectProps<T extends Partial<TIssueFields>> = {
control: Control<T>;
projectId: string | null;
disabled?: boolean;
handleFormChange: () => void;
variant?: TIssueTypeDropdownVariant;
placeholder?: string;
isRequired?: boolean;
renderChevron?: boolean;
dropDownContainerClassName?: string;
showMandatoryFieldInfo?: boolean; // Show info about mandatory fields
handleFormChange?: () => void;
};
export const IssueTypeSelect: React.FC<TIssueTypeSelectProps> = () => <></>;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const IssueTypeSelect = <T extends Partial<TIssueFields>>(props: TIssueTypeSelectProps<T>) => <></>;
@@ -280,6 +280,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
projectId={projectId}
disabled={!!data?.sourceIssueId}
handleFormChange={handleFormChange}
renderChevron
/>
)}
</div>
+1 -8
View File
@@ -55,14 +55,7 @@ export class IssueService extends APIService {
queries?: any,
config = {}
): Promise<TIssuesResponse> {
queries.project_id = projectId;
return this.get(
`/api/workspaces/${workspaceSlug}/v2/issues/`,
{
params: queries,
},
config
)
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/v2/issues/`, { params: queries }, config)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "0.22.0",
"version": "0.23.0",
"private": true,
"scripts": {
"dev": "turbo run develop",
+1694 -1466
View File
File diff suppressed because it is too large Load Diff