From 26de35bd8d31baf0880cf9212061a0468ed8d6dc Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Tue, 7 Nov 2023 17:17:10 +0530 Subject: [PATCH] fix: environment config changes in the API are replicated in web and space app (#2699) * fix: envconfig type changes * chore: configuration variables (#2692) * chore: update avatar group logic (#2672) * chore: configuration variables --------- * fix: replacing slack client id with env config --------- Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com> --- apiserver/plane/api/views/config.py | 6 +- space/components/accounts/sign-in.tsx | 4 +- space/services/app-config.service.ts | 11 +-- turbo.json | 4 +- web/components/integration/github/auth.tsx | 16 ++++- .../integration/single-integration-card.tsx | 16 ++++- .../integration/slack/select-channel.tsx | 19 ++++- web/components/page-views/signin.tsx | 32 +++++---- web/hooks/use-integration-popup.tsx | 25 ++++--- web/lib/mobx/store-init.tsx | 72 ++++++++++--------- web/services/app_config.service.ts | 14 ++-- web/store/app-config.store.ts | 47 ++++++++++++ web/store/root.ts | 3 + web/types/app.d.ts | 9 +++ 14 files changed, 189 insertions(+), 89 deletions(-) create mode 100644 web/store/app-config.store.ts diff --git a/apiserver/plane/api/views/config.py b/apiserver/plane/api/views/config.py index 687cb211c..a06a7f7fc 100644 --- a/apiserver/plane/api/views/config.py +++ b/apiserver/plane/api/views/config.py @@ -21,8 +21,8 @@ class ConfigurationEndpoint(BaseAPIView): def get(self, request): data = {} - data["google"] = os.environ.get("GOOGLE_CLIENT_ID", None) - data["github"] = os.environ.get("GITHUB_CLIENT_ID", None) + data["google_client_id"] = os.environ.get("GOOGLE_CLIENT_ID", None) + data["github_client_id"] = os.environ.get("GITHUB_CLIENT_ID", None) data["github_app_name"] = os.environ.get("GITHUB_APP_NAME", None) data["magic_login"] = ( bool(settings.EMAIL_HOST_USER) and bool(settings.EMAIL_HOST_PASSWORD) @@ -30,5 +30,5 @@ class ConfigurationEndpoint(BaseAPIView): data["email_password_login"] = ( os.environ.get("ENABLE_EMAIL_PASSWORD", "0") == "1" ) - data["slack"] = os.environ.get("SLACK_CLIENT_ID", None) + data["slack_client_id"] = os.environ.get("SLACK_CLIENT_ID", None) return Response(data, status=status.HTTP_200_OK) diff --git a/space/components/accounts/sign-in.tsx b/space/components/accounts/sign-in.tsx index 0c6810198..b55824e6c 100644 --- a/space/components/accounts/sign-in.tsx +++ b/space/components/accounts/sign-in.tsx @@ -116,7 +116,9 @@ export const SignInView = observer(() => { )}
- {data?.google && } + {data?.google_client_id && ( + + )}

diff --git a/space/services/app-config.service.ts b/space/services/app-config.service.ts index 713cda3da..09a6989ef 100644 --- a/space/services/app-config.service.ts +++ b/space/services/app-config.service.ts @@ -3,12 +3,13 @@ import APIService from "services/api.service"; // helper import { API_BASE_URL } from "helpers/common.helper"; -export interface IEnvConfig { - github: string; - google: string; - github_app_name: string | null; +export interface IAppConfig { email_password_login: boolean; + google_client_id: string | null; + github_app_name: string | null; + github_client_id: string | null; magic_login: boolean; + slack_client_id: string | null; } export class AppConfigService extends APIService { @@ -16,7 +17,7 @@ export class AppConfigService extends APIService { super(API_BASE_URL); } - async envConfig(): Promise { + async envConfig(): Promise { return this.get("/api/configs/", { headers: { "Content-Type": "application/json", diff --git a/turbo.json b/turbo.json index 7c3ccb81a..ac462d08b 100644 --- a/turbo.json +++ b/turbo.json @@ -5,7 +5,6 @@ "NEXT_PUBLIC_DEPLOY_URL", "NEXT_PUBLIC_SENTRY_DSN", "NEXT_PUBLIC_SENTRY_ENVIRONMENT", - "NEXT_PUBLIC_GITHUB_APP_NAME", "NEXT_PUBLIC_ENABLE_SENTRY", "NEXT_PUBLIC_ENABLE_OAUTH", "NEXT_PUBLIC_TRACK_EVENTS", @@ -22,8 +21,7 @@ "SLACK_CLIENT_SECRET", "JITSU_TRACKER_ACCESS_KEY", "JITSU_TRACKER_HOST", - "UNSPLASH_ACCESS_KEY", - "NEXT_PUBLIC_SLACK_CLIENT_ID" + "UNSPLASH_ACCESS_KEY" ], "pipeline": { "build": { diff --git a/web/components/integration/github/auth.tsx b/web/components/integration/github/auth.tsx index c94bfacd5..9d5816f3b 100644 --- a/web/components/integration/github/auth.tsx +++ b/web/components/integration/github/auth.tsx @@ -4,14 +4,24 @@ import useIntegrationPopup from "hooks/use-integration-popup"; import { Button } from "@plane/ui"; // types import { IWorkspaceIntegration } from "types"; +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; type Props = { workspaceIntegration: false | IWorkspaceIntegration | undefined; provider: string | undefined; }; -export const GithubAuth: React.FC = ({ workspaceIntegration, provider }) => { - const { startAuth, isConnecting } = useIntegrationPopup(provider); +export const GithubAuth: React.FC = observer(({ workspaceIntegration, provider }) => { + const { + appConfig: { envConfig }, + } = useMobxStore(); + // hooks + const { startAuth, isConnecting } = useIntegrationPopup({ + provider, + github_app_name: envConfig?.github_app_name || "", + slack_client_id: envConfig?.slack_client_id || "", + }); return (

@@ -26,4 +36,4 @@ export const GithubAuth: React.FC = ({ workspaceIntegration, provider }) )}
); -}; +}); diff --git a/web/components/integration/single-integration-card.tsx b/web/components/integration/single-integration-card.tsx index 28fca6fcd..999a12bb5 100644 --- a/web/components/integration/single-integration-card.tsx +++ b/web/components/integration/single-integration-card.tsx @@ -20,6 +20,8 @@ import { CheckCircle } from "lucide-react"; import { IAppIntegration, IWorkspaceIntegration } from "types"; // fetch-keys import { WORKSPACE_INTEGRATIONS } from "constants/fetch-keys"; +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; type Props = { integration: IAppIntegration; @@ -41,7 +43,11 @@ const integrationDetails: { [key: string]: any } = { // services const integrationService = new IntegrationService(); -export const SingleIntegrationCard: React.FC = ({ integration }) => { +export const SingleIntegrationCard: React.FC = observer(({ integration }) => { + const { + appConfig: { envConfig }, + } = useMobxStore(); + const [deletingIntegration, setDeletingIntegration] = useState(false); const router = useRouter(); @@ -49,7 +55,11 @@ export const SingleIntegrationCard: React.FC = ({ integration }) => { const { setToastAlert } = useToast(); - const { startAuth, isConnecting: isInstalling } = useIntegrationPopup(integration.provider); + const { startAuth, isConnecting: isInstalling } = useIntegrationPopup({ + provider: integration.provider, + github_app_name: envConfig?.github_app_name || "", + slack_client_id: envConfig?.slack_client_id || "", + }); const { data: workspaceIntegrations } = useSWR( workspaceSlug ? WORKSPACE_INTEGRATIONS(workspaceSlug as string) : null, @@ -132,4 +142,4 @@ export const SingleIntegrationCard: React.FC = ({ integration }) => { )} ); -}; +}); diff --git a/web/components/integration/slack/select-channel.tsx b/web/components/integration/slack/select-channel.tsx index fb5393f3a..eaaa3daee 100644 --- a/web/components/integration/slack/select-channel.tsx +++ b/web/components/integration/slack/select-channel.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from "react"; import { useRouter } from "next/router"; import useSWR, { mutate } from "swr"; +import { observer } from "mobx-react-lite"; // services import { AppInstallationService } from "services/app_installation.service"; // ui @@ -11,6 +12,8 @@ import useIntegrationPopup from "hooks/use-integration-popup"; import { IWorkspaceIntegration, ISlackIntegration } from "types"; // fetch-keys import { SLACK_CHANNEL_INFO } from "constants/fetch-keys"; +// lib +import { useMobxStore } from "lib/mobx/store-provider"; type Props = { integration: IWorkspaceIntegration; @@ -18,14 +21,24 @@ type Props = { const appInstallationService = new AppInstallationService(); -export const SelectChannel: React.FC = ({ integration }) => { +export const SelectChannel: React.FC = observer(({ integration }) => { + // store + const { + appConfig: { envConfig }, + } = useMobxStore(); + // states const [slackChannelAvailabilityToggle, setSlackChannelAvailabilityToggle] = useState(false); const [slackChannel, setSlackChannel] = useState(null); const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { startAuth } = useIntegrationPopup("slackChannel", integration.id); + const { startAuth } = useIntegrationPopup({ + provider: "slackChannel", + stateParams: integration.id, + github_app_name: envConfig?.github_client_id || "", + slack_client_id: envConfig?.slack_client_id || "", + }); const { data: projectIntegration } = useSWR( workspaceSlug && projectId && integration.id @@ -97,4 +110,4 @@ export const SelectChannel: React.FC = ({ integration }) => { )} ); -}; +}); diff --git a/web/components/page-views/signin.tsx b/web/components/page-views/signin.tsx index ccaa7d2c3..547170632 100644 --- a/web/components/page-views/signin.tsx +++ b/web/components/page-views/signin.tsx @@ -1,5 +1,4 @@ import { useState, useEffect, useCallback } from "react"; -import useSWR from "swr"; import { observer } from "mobx-react-lite"; import Image from "next/image"; import { useRouter } from "next/router"; @@ -8,7 +7,6 @@ import useToast from "hooks/use-toast"; import { useMobxStore } from "lib/mobx/store-provider"; // services import { AuthService } from "services/auth.service"; -import { AppConfigService } from "services/app_config.service"; // components import { GoogleLoginButton, @@ -24,12 +22,12 @@ import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; // types import { IUser, IUserSettings } from "types"; -const appConfigService = new AppConfigService(); const authService = new AuthService(); export const SignInView = observer(() => { const { user: { fetchCurrentUser, fetchCurrentUserSettings }, + appConfig: { envConfig }, } = useMobxStore(); // router const router = useRouter(); @@ -38,12 +36,16 @@ export const SignInView = observer(() => { const [isLoading, setLoading] = useState(false); // toast const { setToastAlert } = useToast(); - // fetch app config - const { data, error: appConfigError } = useSWR("APP_CONFIG", () => appConfigService.envConfig()); // computed const enableEmailPassword = - data && - (data?.email_password_login || !(data?.email_password_login || data?.magic_login || data?.google || data?.github)); + envConfig && + (envConfig?.email_password_login || + !( + envConfig?.email_password_login || + envConfig?.magic_login || + envConfig?.google_client_id || + envConfig?.github_client_id + )); const handleLoginRedirection = useCallback( (user: IUser) => { @@ -114,11 +116,11 @@ export const SignInView = observer(() => { const handleGitHubSignIn = async (credential: string) => { try { setLoading(true); - if (data && data.github && credential) { + if (envConfig && envConfig.github_client_id && credential) { const socialAuthPayload = { medium: "github", credential, - clientId: data.github, + clientId: envConfig.github_client_id, }; const response = await authService.socialAuth(socialAuthPayload); if (response) { @@ -195,7 +197,7 @@ export const SignInView = observer(() => { Sign in to Plane - {!data && !appConfigError ? ( + {!envConfig ? (
@@ -211,7 +213,7 @@ export const SignInView = observer(() => { <> <> {enableEmailPassword && } - {data?.magic_login && ( + {envConfig?.magic_login && (
@@ -219,8 +221,12 @@ export const SignInView = observer(() => {
)}
- {data?.google && } - {data?.github && } + {envConfig?.google_client_id && ( + + )} + {envConfig?.github_client_id && ( + + )}

diff --git a/web/hooks/use-integration-popup.tsx b/web/hooks/use-integration-popup.tsx index 58cfbc009..fb9aab223 100644 --- a/web/hooks/use-integration-popup.tsx +++ b/web/hooks/use-integration-popup.tsx @@ -1,23 +1,26 @@ import { useRef, useState } from "react"; - import { useRouter } from "next/router"; -const useIntegrationPopup = (provider: string | undefined, stateParams?: string) => { +const useIntegrationPopup = ({ + provider, + stateParams, + github_app_name, + slack_client_id, +}: { + provider: string | undefined; + stateParams?: string; + github_app_name?: string; + slack_client_id?: string; +}) => { const [authLoader, setAuthLoader] = useState(false); const router = useRouter(); const { workspaceSlug, projectId } = router.query; const providerUrls: { [key: string]: string } = { - github: `https://github.com/apps/${ - process.env.NEXT_PUBLIC_GITHUB_APP_NAME - }/installations/new?state=${workspaceSlug?.toString()}`, - slack: `https://slack.com/oauth/v2/authorize?scope=chat:write,im:history,im:write,links:read,links:write,users:read,users:read.email&user_scope=&&client_id=${ - process.env.NEXT_PUBLIC_SLACK_CLIENT_ID - }&state=${workspaceSlug?.toString()}`, - slackChannel: `https://slack.com/oauth/v2/authorize?scope=incoming-webhook&client_id=${ - process.env.NEXT_PUBLIC_SLACK_CLIENT_ID - }&state=${workspaceSlug?.toString()},${projectId?.toString()}${ + github: `https://github.com/apps/${github_app_name}/installations/new?state=${workspaceSlug?.toString()}`, + slack: `https://slack.com/oauth/v2/authorize?scope=chat:write,im:history,im:write,links:read,links:write,users:read,users:read.email&user_scope=&&client_id=${slack_client_id}&state=${workspaceSlug?.toString()}`, + slackChannel: `https://slack.com/oauth/v2/authorize?scope=incoming-webhook&client_id=${slack_client_id}&state=${workspaceSlug?.toString()},${projectId?.toString()}${ stateParams ? "," + stateParams : "" }`, }; diff --git a/web/lib/mobx/store-init.tsx b/web/lib/mobx/store-init.tsx index 780b12d99..66d81b5aa 100644 --- a/web/lib/mobx/store-init.tsx +++ b/web/lib/mobx/store-init.tsx @@ -1,11 +1,12 @@ import { useEffect, useState } from "react"; -// next themes +import { observer } from "mobx-react-lite"; +import useSWR from "swr"; import { useTheme } from "next-themes"; +import { useRouter } from "next/router"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; -import { useRouter } from "next/router"; +// helpers import { applyTheme, unsetCustomCssVariables } from "helpers/theme.helper"; -import { observer } from "mobx-react-lite"; const MobxStoreInit = observer(() => { // router @@ -13,16 +14,19 @@ const MobxStoreInit = observer(() => { const { workspaceSlug, projectId, cycleId, moduleId, globalViewId, viewId, inboxId } = router.query; // store const { - theme: themeStore, - user: userStore, - workspace: workspaceStore, - project: projectStore, - cycle: cycleStore, - module: moduleStore, - globalViews: globalViewsStore, - projectViews: projectViewsStore, - inbox: inboxStore, + theme: { sidebarCollapsed, toggleSidebar }, + user: { currentUser }, + workspace: { setWorkspaceSlug }, + project: { setProjectId }, + cycle: { setCycleId }, + module: { setModuleId }, + globalViews: { setGlobalViewId }, + projectViews: { setViewId }, + inbox: { setInboxId }, + appConfig: { fetchAppConfig }, } = useMobxStore(); + // fetching application Config + useSWR("APP_CONFIG", () => fetchAppConfig(), { revalidateIfStale: false, revalidateOnFocus: false }); // state const [dom, setDom] = useState(); // theme @@ -34,36 +38,36 @@ const MobxStoreInit = observer(() => { useEffect(() => { const localValue = localStorage && localStorage.getItem("app_sidebar_collapsed"); const localBoolValue = localValue ? (localValue === "true" ? true : false) : false; - if (localValue && themeStore?.sidebarCollapsed === undefined) { - themeStore.toggleSidebar(localBoolValue); + if (localValue && sidebarCollapsed === undefined) { + toggleSidebar(localBoolValue); } - }, [themeStore, userStore, setTheme]); + }, [sidebarCollapsed, currentUser, setTheme, toggleSidebar]); /** * Setting up the theme of the user by fetching it from local storage */ useEffect(() => { - if (!userStore.currentUser) return; + if (!currentUser) return; if (window) { setDom(window.document?.querySelector("[data-theme='custom']")); } - setTheme(userStore.currentUser?.theme?.theme || "system"); - if (userStore.currentUser?.theme?.theme === "custom" && dom) { - applyTheme(userStore.currentUser?.theme?.palette, false); + setTheme(currentUser?.theme?.theme || "system"); + if (currentUser?.theme?.theme === "custom" && dom) { + applyTheme(currentUser?.theme?.palette, false); } else unsetCustomCssVariables(); - }, [userStore.currentUser, setTheme, dom]); + }, [currentUser, setTheme, dom]); /** * Setting router info to the respective stores. */ useEffect(() => { - if (workspaceSlug) workspaceStore.setWorkspaceSlug(workspaceSlug.toString()); - if (projectId) projectStore.setProjectId(projectId.toString()); - if (cycleId) cycleStore.setCycleId(cycleId.toString()); - if (moduleId) moduleStore.setModuleId(moduleId.toString()); - if (globalViewId) globalViewsStore.setGlobalViewId(globalViewId.toString()); - if (viewId) projectViewsStore.setViewId(viewId.toString()); - if (inboxId) inboxStore.setInboxId(inboxId.toString()); + if (workspaceSlug) setWorkspaceSlug(workspaceSlug.toString()); + if (projectId) setProjectId(projectId.toString()); + if (cycleId) setCycleId(cycleId.toString()); + if (moduleId) setModuleId(moduleId.toString()); + if (globalViewId) setGlobalViewId(globalViewId.toString()); + if (viewId) setViewId(viewId.toString()); + if (inboxId) setInboxId(inboxId.toString()); }, [ workspaceSlug, projectId, @@ -72,13 +76,13 @@ const MobxStoreInit = observer(() => { globalViewId, viewId, inboxId, - workspaceStore, - projectStore, - cycleStore, - moduleStore, - globalViewsStore, - projectViewsStore, - inboxStore, + setWorkspaceSlug, + setProjectId, + setCycleId, + setModuleId, + setGlobalViewId, + setViewId, + setInboxId, ]); return <>; diff --git a/web/services/app_config.service.ts b/web/services/app_config.service.ts index 5843c01c9..8f8bcd423 100644 --- a/web/services/app_config.service.ts +++ b/web/services/app_config.service.ts @@ -2,27 +2,21 @@ import { APIService } from "services/api.service"; // helper import { API_BASE_URL } from "helpers/common.helper"; - -export interface IEnvConfig { - github: string; - google: string; - github_app_name: string | null; - email_password_login: boolean; - magic_login: boolean; -} +// types +import { IAppConfig } from "types/app"; export class AppConfigService extends APIService { constructor() { super(API_BASE_URL); } - async envConfig(): Promise { + async envConfig(): Promise { return this.get("/api/configs/", { headers: { "Content-Type": "application/json", }, }) - .then((response) => response?.data) + .then((response) => response.data) .catch((error) => { throw error?.response?.data; }); diff --git a/web/store/app-config.store.ts b/web/store/app-config.store.ts new file mode 100644 index 000000000..3a4d9efc0 --- /dev/null +++ b/web/store/app-config.store.ts @@ -0,0 +1,47 @@ +import { observable, action, makeObservable, runInAction } from "mobx"; +// types +import { RootStore } from "./root"; +import { IAppConfig } from "types/app"; +// services +import { AppConfigService } from "services/app_config.service"; + +export interface IAppConfigStore { + envConfig: IAppConfig | null; + // action + fetchAppConfig: () => Promise; +} + +class AppConfigStore implements IAppConfigStore { + // observables + envConfig: IAppConfig | null = null; + + // root store + rootStore; + // service + appConfigService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observables + envConfig: observable.ref, + // actions + fetchAppConfig: action, + }); + this.appConfigService = new AppConfigService(); + + this.rootStore = _rootStore; + } + fetchAppConfig = async () => { + try { + const config = await this.appConfigService.envConfig(); + runInAction(() => { + this.envConfig = config; + }); + return config; + } catch (error) { + throw error; + } + }; +} + +export default AppConfigStore; diff --git a/web/store/root.ts b/web/store/root.ts index 9af4db492..c6d781b28 100644 --- a/web/store/root.ts +++ b/web/store/root.ts @@ -1,5 +1,6 @@ import { enableStaticRendering } from "mobx-react-lite"; // store imports +import AppConfigStore, { IAppConfigStore } from "./app-config.store"; import CommandPaletteStore, { ICommandPaletteStore } from "./command-palette.store"; import UserStore, { IUserStore } from "store/user.store"; import ThemeStore, { IThemeStore } from "store/theme.store"; @@ -107,6 +108,7 @@ enableStaticRendering(typeof window === "undefined"); export class RootStore { user: IUserStore; theme: IThemeStore; + appConfig: IAppConfigStore; commandPalette: ICommandPaletteStore; workspace: IWorkspaceStore; @@ -167,6 +169,7 @@ export class RootStore { mentionsStore: IMentionsStore; constructor() { + this.appConfig = new AppConfigStore(this); this.commandPalette = new CommandPaletteStore(this); this.user = new UserStore(this); this.theme = new ThemeStore(this); diff --git a/web/types/app.d.ts b/web/types/app.d.ts index 2b03f6975..c762fb76f 100644 --- a/web/types/app.d.ts +++ b/web/types/app.d.ts @@ -1,3 +1,12 @@ export type NextPageWithLayout

= NextPage & { getLayout?: (page: ReactElement) => ReactNode; }; + +export interface IAppConfig { + email_password_login: boolean; + google_client_id: string | null; + github_app_name: string | null; + github_client_id: string | null; + magic_login: boolean; + slack_client_id: string | null; +}