Skip to content

Commit 7c74d0a

Browse files
[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 <[email protected]>
1 parent 5f7ffcb commit 7c74d0a

File tree

16 files changed

+373
-61
lines changed

16 files changed

+373
-61
lines changed

apps/api/plane/license/api/views/instance.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ def get(self, request):
175175
data["app_base_url"] = settings.APP_BASE_URL
176176

177177
data["instance_changelog_url"] = settings.INSTANCE_CHANGELOG_URL
178+
data["is_self_managed"] = settings.IS_SELF_MANAGED
178179

179180
instance_data = serializer.data
180181
instance_data["workspaces_exist"] = Workspace.objects.count() >= 1

apps/api/plane/settings/common.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
# SECURITY WARNING: don't run with debug turned on in production!
2626
DEBUG = int(os.environ.get("DEBUG", "0"))
2727

28+
# Self-hosted mode
29+
IS_SELF_MANAGED = True
30+
2831
# Allowed Hosts
2932
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "*").split(",")
3033

@@ -69,9 +72,7 @@
6972

7073
# Rest Framework settings
7174
REST_FRAMEWORK = {
72-
"DEFAULT_AUTHENTICATION_CLASSES": (
73-
"rest_framework.authentication.SessionAuthentication",
74-
),
75+
"DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework.authentication.SessionAuthentication",),
7576
"DEFAULT_THROTTLE_CLASSES": ("rest_framework.throttling.AnonRateThrottle",),
7677
"DEFAULT_THROTTLE_RATES": {
7778
"anon": "30/minute",
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
export * from "./version-number";
2-
export * from "./product-updates-header";
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { useState, useEffect, useRef } from "react";
2+
import { observer } from "mobx-react";
3+
// hooks
4+
import { Loader } from "@plane/ui";
5+
import { ProductUpdatesFallback } from "@/components/global/product-updates/fallback";
6+
import { useInstance } from "@/hooks/store/use-instance";
7+
8+
export const ProductUpdatesChangelog = observer(function ProductUpdatesChangelog() {
9+
// refs
10+
const isLoadingRef = useRef(true);
11+
// states
12+
const [isLoading, setIsLoading] = useState(true);
13+
const [hasError, setHasError] = useState(false);
14+
// store hooks
15+
const { config } = useInstance();
16+
// derived values
17+
const changeLogUrl = config?.instance_changelog_url;
18+
const shouldShowFallback = !changeLogUrl || changeLogUrl === "" || hasError;
19+
20+
// timeout fallback - if iframe doesn't load within 15 seconds, show error
21+
useEffect(() => {
22+
if (!changeLogUrl || changeLogUrl === "") {
23+
setIsLoading(false);
24+
isLoadingRef.current = false;
25+
return;
26+
}
27+
28+
setIsLoading(true);
29+
setHasError(false);
30+
isLoadingRef.current = true;
31+
32+
const timeoutId = setTimeout(() => {
33+
if (isLoadingRef.current) {
34+
setHasError(true);
35+
setIsLoading(false);
36+
isLoadingRef.current = false;
37+
}
38+
}, 15000); // 15 second timeout
39+
40+
return () => {
41+
clearTimeout(timeoutId);
42+
};
43+
}, [changeLogUrl]);
44+
45+
const handleIframeLoad = () => {
46+
setTimeout(() => {
47+
isLoadingRef.current = false;
48+
setIsLoading(false);
49+
}, 1000);
50+
};
51+
52+
const handleIframeError = () => {
53+
isLoadingRef.current = false;
54+
setHasError(true);
55+
setIsLoading(false);
56+
};
57+
58+
// Show fallback if URL is missing, empty, or iframe failed to load
59+
if (shouldShowFallback) {
60+
return (
61+
<ProductUpdatesFallback
62+
description="We're having trouble fetching the updates. Please visit our changelog to view the latest updates."
63+
variant={config?.is_self_managed ? "self-managed" : "cloud"}
64+
/>
65+
);
66+
}
67+
68+
return (
69+
<div className="flex flex-col h-[550px] vertical-scrollbar scrollbar-xs overflow-hidden overflow-y-scroll px-6 mx-0.5 relative">
70+
{isLoading && (
71+
<Loader className="flex flex-col gap-3 absolute inset-0 w-full h-full items-center justify-center">
72+
<Loader.Item height="95%" width="95%" />
73+
</Loader>
74+
)}
75+
<iframe
76+
src={changeLogUrl}
77+
className={`w-full h-full ${isLoading ? "opacity-0" : "opacity-100"} transition-opacity duration-200`}
78+
onLoad={handleIframeLoad}
79+
onError={handleIframeError}
80+
/>
81+
</div>
82+
);
83+
});

apps/web/ce/components/global/product-updates-header.tsx renamed to apps/web/ce/components/global/product-updates/header.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { observer } from "mobx-react";
2+
import packageJson from "package.json";
23
import { useTranslation } from "@plane/i18n";
3-
import { PlaneLogo } from "@plane/propel/icons";
44
// helpers
55
import { cn } from "@plane/utils";
6-
// package.json
7-
import packageJson from "package.json";
86

97
export const ProductUpdatesHeader = observer(function ProductUpdatesHeader() {
108
const { t } = useTranslation();
@@ -20,9 +18,6 @@ export const ProductUpdatesHeader = observer(function ProductUpdatesHeader() {
2018
{t("version")}: v{packageJson.version}
2119
</div>
2220
</div>
23-
<div className="flex flex-shrink-0 items-center gap-8">
24-
<PlaneLogo className="h-6 w-auto text-custom-text-100" />
25-
</div>
2621
</div>
2722
);
2823
});

apps/web/core/components/core/image-picker-popover.tsx

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useRef, useCallback } from "react";
1+
import React, { useState, useRef, useCallback, useMemo } from "react";
22
import { observer } from "mobx-react";
33
import { useParams } from "next/navigation";
44
import { useDropzone } from "react-dropzone";
@@ -16,24 +16,16 @@ import { Input, Loader } from "@plane/ui";
1616
// helpers
1717
import { getFileURL } from "@plane/utils";
1818
// hooks
19+
import { useInstance } from "@/hooks/store/use-instance";
1920
import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
2021
// services
2122
import { FileService } from "@/services/file.service";
2223

23-
const tabOptions = [
24-
{
25-
key: "unsplash",
26-
title: "Unsplash",
27-
},
28-
{
29-
key: "images",
30-
title: "Images",
31-
},
32-
{
33-
key: "upload",
34-
title: "Upload",
35-
},
36-
];
24+
type TTabOption = {
25+
key: string;
26+
title: string;
27+
isEnabled: boolean;
28+
};
3729

3830
type Props = {
3931
label: string | React.ReactNode;
@@ -63,6 +55,30 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
6355
const ref = useRef<HTMLDivElement>(null);
6456
// router params
6557
const { workspaceSlug } = useParams();
58+
// store hooks
59+
const { config } = useInstance();
60+
// derived values
61+
const hasUnsplashConfigured = config?.has_unsplash_configured || false;
62+
const tabOptions: TTabOption[] = useMemo(
63+
() => [
64+
{
65+
key: "unsplash",
66+
title: "Unsplash",
67+
isEnabled: hasUnsplashConfigured,
68+
},
69+
{
70+
key: "images",
71+
title: "Images",
72+
isEnabled: true,
73+
},
74+
{
75+
key: "upload",
76+
title: "Upload",
77+
isEnabled: true,
78+
},
79+
],
80+
[hasUnsplashConfigured]
81+
);
6682

6783
const { data: unsplashImages, error: unsplashError } = useSWR(
6884
`UNSPLASH_IMAGES_${searchParams}`,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { EmptyStateDetailed } from "@plane/propel/empty-state";
2+
3+
type TProductUpdatesFallbackProps = {
4+
description: string;
5+
variant: "cloud" | "self-managed";
6+
};
7+
8+
export function ProductUpdatesFallback(props: TProductUpdatesFallbackProps) {
9+
const { description, variant } = props;
10+
// derived values
11+
const changelogUrl =
12+
variant === "cloud"
13+
? "https://plane.so/changelog?category=cloud"
14+
: "https://plane.so/changelog?category=self-hosted";
15+
16+
return (
17+
<div className="py-8">
18+
<EmptyStateDetailed
19+
assetKey="changelog"
20+
description={description}
21+
align="center"
22+
actions={[
23+
{
24+
label: "Go to changelog",
25+
variant: "primary",
26+
onClick: () => window.open(changelogUrl, "_blank"),
27+
},
28+
]}
29+
/>
30+
</div>
31+
);
32+
}

apps/web/core/components/global/product-updates/modal.tsx

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
1-
import type { FC } from "react";
21
import { useEffect } from "react";
32
import { observer } from "mobx-react";
43
import { USER_TRACKER_ELEMENTS } from "@plane/constants";
5-
import { useTranslation } from "@plane/i18n";
64
// ui
75
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
86
// components
97
import { ProductUpdatesFooter } from "@/components/global";
108
// helpers
119
import { captureView } from "@/helpers/event-tracker.helper";
12-
// hooks
13-
import { useInstance } from "@/hooks/store/use-instance";
1410
// plane web components
15-
import { ProductUpdatesHeader } from "@/plane-web/components/global";
11+
import { ProductUpdatesChangelog } from "@/plane-web/components/global/product-updates/changelog";
12+
import { ProductUpdatesHeader } from "@/plane-web/components/global/product-updates/header";
1613

1714
export type ProductUpdatesModalProps = {
1815
isOpen: boolean;
@@ -21,8 +18,6 @@ export type ProductUpdatesModalProps = {
2118

2219
export const ProductUpdatesModal = observer(function ProductUpdatesModal(props: ProductUpdatesModalProps) {
2320
const { isOpen, handleClose } = props;
24-
const { t } = useTranslation();
25-
const { config } = useInstance();
2621

2722
useEffect(() => {
2823
if (isOpen) {
@@ -33,27 +28,7 @@ export const ProductUpdatesModal = observer(function ProductUpdatesModal(props:
3328
return (
3429
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXXXL}>
3530
<ProductUpdatesHeader />
36-
<div className="flex flex-col h-[60vh] vertical-scrollbar scrollbar-xs overflow-hidden overflow-y-scroll px-6 mx-0.5">
37-
{config?.instance_changelog_url && config?.instance_changelog_url !== "" ? (
38-
<iframe src={config?.instance_changelog_url} className="w-full h-full" />
39-
) : (
40-
<div className="flex flex-col items-center justify-center w-full h-full mb-8">
41-
<div className="text-lg font-medium">{t("we_are_having_trouble_fetching_the_updates")}</div>
42-
<div className="text-sm text-custom-text-200">
43-
{t("please_visit")}
44-
<a
45-
data-ph-element={USER_TRACKER_ELEMENTS.CHANGELOG_REDIRECTED}
46-
href="https://go.plane.so/p-changelog"
47-
target="_blank"
48-
className="text-sm text-custom-primary-100 font-medium hover:text-custom-primary-200 underline underline-offset-1 outline-none"
49-
>
50-
{t("our_changelogs")}
51-
</a>{" "}
52-
{t("for_the_latest_updates")}.
53-
</div>
54-
</div>
55-
)}
56-
</div>
31+
<ProductUpdatesChangelog />
5732
<ProductUpdatesFooter />
5833
</ModalCore>
5934
);

apps/web/core/components/onboarding/steps/profile/root.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { AuthService } from "@/services/auth.service";
2222
import { CommonOnboardingHeader } from "../common";
2323
import { MarketingConsent } from "./consent";
2424
import { SetPasswordRoot } from "./set-password";
25+
import { useInstance } from "@/hooks/store/use-instance";
2526

2627
type Props = {
2728
handleStepChange: (step: EOnboardingSteps, skipInvites?: boolean) => void;
@@ -55,6 +56,7 @@ export const ProfileSetupStep = observer(function ProfileSetupStep({ handleStepC
5556
// store hooks
5657
const { data: user, updateCurrentUser } = useUser();
5758
const { updateUserProfile } = useUserProfile();
59+
const { config: instanceConfig } = useInstance();
5860
// form info
5961
const {
6062
getValues,
@@ -253,12 +255,14 @@ export const ProfileSetupStep = observer(function ProfileSetupStep({ handleStepC
253255
</Button>
254256

255257
{/* Marketing Consent */}
256-
<MarketingConsent
257-
isChecked={!!watch("has_marketing_email_consent")}
258-
handleChange={(has_marketing_email_consent) =>
259-
setValue("has_marketing_email_consent", has_marketing_email_consent)
260-
}
261-
/>
258+
{!instanceConfig.is_self_managed && (
259+
<MarketingConsent
260+
isChecked={!!watch("has_marketing_email_consent")}
261+
handleChange={(has_marketing_email_consent) =>
262+
setValue("has_marketing_email_consent", has_marketing_email_consent)
263+
}
264+
/>
265+
)}
262266
</form>
263267
);
264268
});

packages/propel/src/empty-state/assets/asset-registry.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
ArchivedCycleVerticalStackIllustration,
3434
ArchivedModuleVerticalStackIllustration,
3535
ArchivedWorkItemVerticalStackIllustration,
36+
ChangelogVerticalStackIllustration,
3637
CustomerVerticalStackIllustration,
3738
CycleVerticalStackIllustration,
3839
DashboardVerticalStackIllustration,
@@ -80,6 +81,7 @@ export const VERTICAL_STACK_ASSETS: Record<VerticalStackAssetType, React.Compone
8081
"archived-cycle": ArchivedCycleVerticalStackIllustration,
8182
"archived-module": ArchivedModuleVerticalStackIllustration,
8283
"archived-work-item": ArchivedWorkItemVerticalStackIllustration,
84+
changelog: ChangelogVerticalStackIllustration,
8385
customer: CustomerVerticalStackIllustration,
8486
cycle: CycleVerticalStackIllustration,
8587
dashboard: DashboardVerticalStackIllustration,

0 commit comments

Comments
 (0)