Skip to content
Merged
1 change: 1 addition & 0 deletions apps/api/plane/license/api/views/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ def get(self, request):
data["app_base_url"] = settings.APP_BASE_URL

data["instance_changelog_url"] = settings.INSTANCE_CHANGELOG_URL
data["is_self_managed"] = settings.IS_SELF_MANAGED

instance_data = serializer.data
instance_data["workspaces_exist"] = Workspace.objects.count() >= 1
Expand Down
7 changes: 4 additions & 3 deletions apps/api/plane/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = int(os.environ.get("DEBUG", "0"))

# Self-hosted mode
IS_SELF_MANAGED = True

Comment on lines +28 to +30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Hardcoded IS_SELF_MANAGED flag prevents dynamic deployment detection.

The IS_SELF_MANAGED flag is hardcoded to True, which prevents runtime detection of deployment type. If the same codebase serves both cloud and self-hosted deployments, this will incorrectly identify cloud instances as self-managed.

Consider making this configurable via environment variable:

-# Self-hosted mode
-IS_SELF_MANAGED = True
+# Self-managed mode
+IS_SELF_MANAGED = os.environ.get("IS_SELF_MANAGED", "1") == "1"

This allows cloud deployments to set IS_SELF_MANAGED=0 while self-hosted instances default to self-managed mode.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Self-hosted mode
IS_SELF_MANAGED = True
# Self-managed mode
IS_SELF_MANAGED = os.environ.get("IS_SELF_MANAGED", "1") == "1"
🤖 Prompt for AI Agents
In apps/api/plane/settings/common.py around lines 28-30 the IS_SELF_MANAGED flag
is hardcoded to True which prevents runtime deployment detection; change it to
read an environment variable (e.g., IS_SELF_MANAGED) with a sensible default of
True for self-hosted, parse common truthy/falsey values (like "1","true","yes"
=> True; "0","false","no" => False) and assign the resulting boolean to
IS_SELF_MANAGED so cloud deployments can override via environment without
changing code.

# Allowed Hosts
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "*").split(",")

Expand Down Expand Up @@ -69,9 +72,7 @@

# Rest Framework settings
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework.authentication.SessionAuthentication",
),
"DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework.authentication.SessionAuthentication",),
"DEFAULT_THROTTLE_CLASSES": ("rest_framework.throttling.AnonRateThrottle",),
"DEFAULT_THROTTLE_RATES": {
"anon": "30/minute",
Expand Down
1 change: 0 additions & 1 deletion apps/web/ce/components/global/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from "./version-number";
export * from "./product-updates-header";
83 changes: 83 additions & 0 deletions apps/web/ce/components/global/product-updates/changelog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { useState, useEffect, useRef } from "react";
import { observer } from "mobx-react";
// hooks
import { Loader } from "@plane/ui";
import { ProductUpdatesFallback } from "@/components/global/product-updates/fallback";
import { useInstance } from "@/hooks/store/use-instance";

export const ProductUpdatesChangelog = observer(function ProductUpdatesChangelog() {
// refs
const isLoadingRef = useRef(true);
// states
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
// store hooks
const { config } = useInstance();
// derived values
const changeLogUrl = config?.instance_changelog_url;
const shouldShowFallback = !changeLogUrl || changeLogUrl === "" || hasError;

// timeout fallback - if iframe doesn't load within 15 seconds, show error
useEffect(() => {
if (!changeLogUrl || changeLogUrl === "") {
setIsLoading(false);
isLoadingRef.current = false;
return;
}

setIsLoading(true);
setHasError(false);
isLoadingRef.current = true;

const timeoutId = setTimeout(() => {
if (isLoadingRef.current) {
setHasError(true);
setIsLoading(false);
isLoadingRef.current = false;
}
}, 15000); // 15 second timeout

return () => {
clearTimeout(timeoutId);
};
}, [changeLogUrl]);

const handleIframeLoad = () => {
setTimeout(() => {
isLoadingRef.current = false;
setIsLoading(false);
}, 1000);
};

const handleIframeError = () => {
isLoadingRef.current = false;
setHasError(true);
setIsLoading(false);
};

// Show fallback if URL is missing, empty, or iframe failed to load
if (shouldShowFallback) {
return (
<ProductUpdatesFallback
description="We're having trouble fetching the updates. Please visit our changelog to view the latest updates."
variant={config?.is_self_managed ? "self-managed" : "cloud"}
/>
);
}

return (
<div className="flex flex-col h-[550px] vertical-scrollbar scrollbar-xs overflow-hidden overflow-y-scroll px-6 mx-0.5 relative">
{isLoading && (
<Loader className="flex flex-col gap-3 absolute inset-0 w-full h-full items-center justify-center">
<Loader.Item height="95%" width="95%" />
</Loader>
)}
<iframe
src={changeLogUrl}
className={`w-full h-full ${isLoading ? "opacity-0" : "opacity-100"} transition-opacity duration-200`}
onLoad={handleIframeLoad}
onError={handleIframeError}
/>
</div>
);
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { observer } from "mobx-react";
import packageJson from "package.json";
import { useTranslation } from "@plane/i18n";
import { PlaneLogo } from "@plane/propel/icons";
// helpers
import { cn } from "@plane/utils";
// package.json
import packageJson from "package.json";

export const ProductUpdatesHeader = observer(function ProductUpdatesHeader() {
const { t } = useTranslation();
Expand All @@ -20,9 +18,6 @@ export const ProductUpdatesHeader = observer(function ProductUpdatesHeader() {
{t("version")}: v{packageJson.version}
</div>
</div>
<div className="flex flex-shrink-0 items-center gap-8">
<PlaneLogo className="h-6 w-auto text-custom-text-100" />
</div>
</div>
);
});
46 changes: 31 additions & 15 deletions apps/web/core/components/core/image-picker-popover.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useRef, useCallback } from "react";
import React, { useState, useRef, useCallback, useMemo } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { useDropzone } from "react-dropzone";
Expand All @@ -16,24 +16,16 @@ import { Input, Loader } from "@plane/ui";
// helpers
import { getFileURL } from "@plane/utils";
// hooks
import { useInstance } from "@/hooks/store/use-instance";
import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
// services
import { FileService } from "@/services/file.service";

const tabOptions = [
{
key: "unsplash",
title: "Unsplash",
},
{
key: "images",
title: "Images",
},
{
key: "upload",
title: "Upload",
},
];
type TTabOption = {
key: string;
title: string;
isEnabled: boolean;
};

type Props = {
label: string | React.ReactNode;
Expand Down Expand Up @@ -63,6 +55,30 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
const ref = useRef<HTMLDivElement>(null);
// router params
const { workspaceSlug } = useParams();
// store hooks
const { config } = useInstance();
// derived values
const hasUnsplashConfigured = config?.has_unsplash_configured || false;
const tabOptions: TTabOption[] = useMemo(
() => [
{
key: "unsplash",
title: "Unsplash",
isEnabled: hasUnsplashConfigured,
},
{
key: "images",
title: "Images",
isEnabled: true,
},
{
key: "upload",
title: "Upload",
isEnabled: true,
},
],
[hasUnsplashConfigured]
);

const { data: unsplashImages, error: unsplashError } = useSWR(
`UNSPLASH_IMAGES_${searchParams}`,
Expand Down
32 changes: 32 additions & 0 deletions apps/web/core/components/global/product-updates/fallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { EmptyStateDetailed } from "@plane/propel/empty-state";

type TProductUpdatesFallbackProps = {
description: string;
variant: "cloud" | "self-managed";
};

export function ProductUpdatesFallback(props: TProductUpdatesFallbackProps) {
const { description, variant } = props;
// derived values
const changelogUrl =
variant === "cloud"
? "https://plane.so/changelog?category=cloud"
: "https://plane.so/changelog?category=self-hosted";

return (
<div className="py-8">
<EmptyStateDetailed
assetKey="changelog"
description={description}
align="center"
actions={[
{
label: "Go to changelog",
variant: "primary",
onClick: () => window.open(changelogUrl, "_blank"),
},
]}
/>
</div>
);
}
31 changes: 3 additions & 28 deletions apps/web/core/components/global/product-updates/modal.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import type { FC } from "react";
import { useEffect } from "react";
import { observer } from "mobx-react";
import { USER_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// components
import { ProductUpdatesFooter } from "@/components/global";
// helpers
import { captureView } from "@/helpers/event-tracker.helper";
// hooks
import { useInstance } from "@/hooks/store/use-instance";
// plane web components
import { ProductUpdatesHeader } from "@/plane-web/components/global";
import { ProductUpdatesChangelog } from "@/plane-web/components/global/product-updates/changelog";
import { ProductUpdatesHeader } from "@/plane-web/components/global/product-updates/header";

export type ProductUpdatesModalProps = {
isOpen: boolean;
Expand All @@ -21,8 +18,6 @@ export type ProductUpdatesModalProps = {

export const ProductUpdatesModal = observer(function ProductUpdatesModal(props: ProductUpdatesModalProps) {
const { isOpen, handleClose } = props;
const { t } = useTranslation();
const { config } = useInstance();

useEffect(() => {
if (isOpen) {
Expand All @@ -33,27 +28,7 @@ export const ProductUpdatesModal = observer(function ProductUpdatesModal(props:
return (
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXXXL}>
<ProductUpdatesHeader />
<div className="flex flex-col h-[60vh] vertical-scrollbar scrollbar-xs overflow-hidden overflow-y-scroll px-6 mx-0.5">
{config?.instance_changelog_url && config?.instance_changelog_url !== "" ? (
<iframe src={config?.instance_changelog_url} className="w-full h-full" />
) : (
<div className="flex flex-col items-center justify-center w-full h-full mb-8">
<div className="text-lg font-medium">{t("we_are_having_trouble_fetching_the_updates")}</div>
<div className="text-sm text-custom-text-200">
{t("please_visit")}
<a
data-ph-element={USER_TRACKER_ELEMENTS.CHANGELOG_REDIRECTED}
href="https://go.plane.so/p-changelog"
target="_blank"
className="text-sm text-custom-primary-100 font-medium hover:text-custom-primary-200 underline underline-offset-1 outline-none"
>
{t("our_changelogs")}
</a>{" "}
{t("for_the_latest_updates")}.
</div>
</div>
)}
</div>
<ProductUpdatesChangelog />
<ProductUpdatesFooter />
</ModalCore>
);
Expand Down
16 changes: 10 additions & 6 deletions apps/web/core/components/onboarding/steps/profile/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { AuthService } from "@/services/auth.service";
import { CommonOnboardingHeader } from "../common";
import { MarketingConsent } from "./consent";
import { SetPasswordRoot } from "./set-password";
import { useInstance } from "@/hooks/store/use-instance";

type Props = {
handleStepChange: (step: EOnboardingSteps, skipInvites?: boolean) => void;
Expand Down Expand Up @@ -55,6 +56,7 @@ export const ProfileSetupStep = observer(function ProfileSetupStep({ handleStepC
// store hooks
const { data: user, updateCurrentUser } = useUser();
const { updateUserProfile } = useUserProfile();
const { config: instanceConfig } = useInstance();
// form info
const {
getValues,
Expand Down Expand Up @@ -253,12 +255,14 @@ export const ProfileSetupStep = observer(function ProfileSetupStep({ handleStepC
</Button>

{/* Marketing Consent */}
<MarketingConsent
isChecked={!!watch("has_marketing_email_consent")}
handleChange={(has_marketing_email_consent) =>
setValue("has_marketing_email_consent", has_marketing_email_consent)
}
/>
{!instanceConfig.is_self_managed && (
<MarketingConsent
isChecked={!!watch("has_marketing_email_consent")}
handleChange={(has_marketing_email_consent) =>
setValue("has_marketing_email_consent", has_marketing_email_consent)
}
/>
)}
</form>
);
});
2 changes: 2 additions & 0 deletions packages/propel/src/empty-state/assets/asset-registry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
ArchivedCycleVerticalStackIllustration,
ArchivedModuleVerticalStackIllustration,
ArchivedWorkItemVerticalStackIllustration,
ChangelogVerticalStackIllustration,
CustomerVerticalStackIllustration,
CycleVerticalStackIllustration,
DashboardVerticalStackIllustration,
Expand Down Expand Up @@ -80,6 +81,7 @@ export const VERTICAL_STACK_ASSETS: Record<VerticalStackAssetType, React.Compone
"archived-cycle": ArchivedCycleVerticalStackIllustration,
"archived-module": ArchivedModuleVerticalStackIllustration,
"archived-work-item": ArchivedWorkItemVerticalStackIllustration,
changelog: ChangelogVerticalStackIllustration,
customer: CustomerVerticalStackIllustration,
cycle: CycleVerticalStackIllustration,
dashboard: DashboardVerticalStackIllustration,
Expand Down
1 change: 1 addition & 0 deletions packages/propel/src/empty-state/assets/asset-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type VerticalStackAssetType =
| "archived-cycle"
| "archived-module"
| "archived-work-item"
| "changelog"
| "customer"
| "cycle"
| "dashboard"
Expand Down
Loading
Loading