From 7c74d0a4034b470211d5ab6032d4915e05fcaf05 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Wed, 3 Dec 2025 15:53:42 +0530 Subject: [PATCH] [WEB-5290] feat: selfhosted check (#8227) * feat: add in common py * fix: update marketing consent screen based on is self managed flag * improvement: enhance ImagePickerPopover with dynamic tab options based on Unsplash configuration * refactor: product updates modal to include changelog * [WEB-5290] feat: implement fallback for product updates changelog with loading state and error handling --------- Co-authored-by: sriramveeraghanta --- apps/api/plane/license/api/views/instance.py | 1 + apps/api/plane/settings/common.py | 7 +- apps/web/ce/components/global/index.ts | 1 - .../global/product-updates/changelog.tsx | 83 ++++++++ .../header.tsx} | 7 +- .../components/core/image-picker-popover.tsx | 46 +++-- .../global/product-updates/fallback.tsx | 32 +++ .../global/product-updates/modal.tsx | 31 +-- .../onboarding/steps/profile/root.tsx | 16 +- .../src/empty-state/assets/asset-registry.tsx | 2 + .../src/empty-state/assets/asset-types.ts | 1 + .../assets/vertical-stack/changelog.tsx | 183 ++++++++++++++++++ .../assets/vertical-stack/constant.tsx | 5 + .../assets/vertical-stack/index.ts | 1 + .../src/empty-state/detailed-empty-state.tsx | 17 +- packages/types/src/instance/base.ts | 1 + 16 files changed, 373 insertions(+), 61 deletions(-) create mode 100644 apps/web/ce/components/global/product-updates/changelog.tsx rename apps/web/ce/components/global/{product-updates-header.tsx => product-updates/header.tsx} (79%) create mode 100644 apps/web/core/components/global/product-updates/fallback.tsx create mode 100644 packages/propel/src/empty-state/assets/vertical-stack/changelog.tsx diff --git a/apps/api/plane/license/api/views/instance.py b/apps/api/plane/license/api/views/instance.py index 23eeebec1..fed0c5e17 100644 --- a/apps/api/plane/license/api/views/instance.py +++ b/apps/api/plane/license/api/views/instance.py @@ -175,6 +175,7 @@ class InstanceEndpoint(BaseAPIView): 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 diff --git a/apps/api/plane/settings/common.py b/apps/api/plane/settings/common.py index 417805216..a9e9925c2 100644 --- a/apps/api/plane/settings/common.py +++ b/apps/api/plane/settings/common.py @@ -25,6 +25,9 @@ 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")) +# Self-hosted mode +IS_SELF_MANAGED = True + # Allowed Hosts ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "*").split(",") @@ -69,9 +72,7 @@ MIDDLEWARE = [ # 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", diff --git a/apps/web/ce/components/global/index.ts b/apps/web/ce/components/global/index.ts index c87c8ae02..08b85c764 100644 --- a/apps/web/ce/components/global/index.ts +++ b/apps/web/ce/components/global/index.ts @@ -1,2 +1 @@ export * from "./version-number"; -export * from "./product-updates-header"; diff --git a/apps/web/ce/components/global/product-updates/changelog.tsx b/apps/web/ce/components/global/product-updates/changelog.tsx new file mode 100644 index 000000000..672b7490b --- /dev/null +++ b/apps/web/ce/components/global/product-updates/changelog.tsx @@ -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 ( + + ); + } + + return ( +
+ {isLoading && ( + + + + )} +