diff --git a/app/client/src/AppRouter.tsx b/app/client/src/AppRouter.tsx index 77fbbf566b..82dff92748 100644 --- a/app/client/src/AppRouter.tsx +++ b/app/client/src/AppRouter.tsx @@ -48,14 +48,15 @@ import { trimTrailingSlash } from "utils/helpers"; import { getSafeCrash, getSafeCrashCode } from "selectors/errorSelectors"; import UserProfile from "pages/UserProfile"; import { getCurrentUser } from "actions/authActions"; -import { getFeatureFlagsFetched } from "selectors/usersSelectors"; +import { selectFeatureFlags } from "selectors/usersSelectors"; import Setup from "pages/setup"; import Settings from "pages/Settings"; import SignupSuccess from "pages/setup/SignupSuccess"; import { Theme } from "constants/DefaultTheme"; import { ERROR_CODES } from "ce/constants/ApiConstants"; import TemplatesListLoader from "pages/Templates/loader"; -import getFeatureFlags from "utils/featureFlags"; +import { fetchFeatureFlagsInit } from "actions/userActions"; +import FeatureFlags from "entities/FeatureFlags"; const SentryRoute = Sentry.withSentryRouting(Route); @@ -78,19 +79,21 @@ function changeAppBackground(currentTheme: any) { function AppRouter(props: { safeCrash: boolean; getCurrentUser: () => void; + getFeatureFlags: () => void; currentTheme: Theme; - featureFlagsFetched: boolean; safeCrashCode?: ERROR_CODES; + featureFlags: FeatureFlags; setTheme: (theme: ThemeMode) => void; }) { + const { featureFlags, getCurrentUser, getFeatureFlags } = props; useEffect(() => { AnalyticsUtil.logEvent("ROUTE_CHANGE", { path: window.location.pathname }); const stopListener = history.listen((location: any) => { AnalyticsUtil.logEvent("ROUTE_CHANGE", { path: location.pathname }); changeAppBackground(props.currentTheme); }); - props.getCurrentUser(); - + getCurrentUser(); + getFeatureFlags(); return stopListener; }, []); @@ -98,8 +101,6 @@ function AppRouter(props: { changeAppBackground(props.currentTheme); }, [props.currentTheme]); - if (!props.featureFlagsFetched) return null; - return ( @@ -128,14 +129,13 @@ function AppRouter(props: { exact path={SIGNUP_SUCCESS_URL} /> - - {getFeatureFlags().APP_TEMPLATE && ( + {featureFlags.APP_TEMPLATE && ( ({ currentTheme: getCurrentThemeDetails(state), safeCrash: getSafeCrash(state), safeCrashCode: getSafeCrashCode(state), - featureFlagsFetched: getFeatureFlagsFetched(state), + featureFlags: selectFeatureFlags(state), }); const mapDispatchToProps = (dispatch: any) => ({ @@ -182,6 +182,7 @@ const mapDispatchToProps = (dispatch: any) => ({ dispatch(setThemeMode(mode)); }, getCurrentUser: () => dispatch(getCurrentUser()), + getFeatureFlags: () => dispatch(fetchFeatureFlagsInit()), }); export default connect(mapStateToProps, mapDispatchToProps)(AppRouter); diff --git a/app/client/src/actions/userActions.ts b/app/client/src/actions/userActions.ts index 11fd481a5a..d3b09204c1 100644 --- a/app/client/src/actions/userActions.ts +++ b/app/client/src/actions/userActions.ts @@ -11,6 +11,7 @@ import { UpdateUserRequest, VerifyTokenRequest, } from "api/UserApi"; +import FeatureFlags from "entities/FeatureFlags"; export const logoutUser = (payload?: { redirectURL: string }) => ({ type: ReduxActionTypes.LOGOUT_USER_INIT, @@ -108,8 +109,9 @@ export const fetchFeatureFlagsInit = () => ({ type: ReduxActionTypes.FETCH_FEATURE_FLAGS_INIT, }); -export const fetchFeatureFlagsSuccess = () => ({ +export const fetchFeatureFlagsSuccess = (payload: FeatureFlags) => ({ type: ReduxActionTypes.FETCH_FEATURE_FLAGS_SUCCESS, + payload, }); export const fetchFeatureFlagsError = (error: any) => ({ diff --git a/app/client/src/components/designSystems/appsmith/header/DeployLinkButton.tsx b/app/client/src/components/designSystems/appsmith/header/DeployLinkButton.tsx index 8bb9edb92b..2be76a1a2e 100644 --- a/app/client/src/components/designSystems/appsmith/header/DeployLinkButton.tsx +++ b/app/client/src/components/designSystems/appsmith/header/DeployLinkButton.tsx @@ -4,13 +4,13 @@ import { Icon, Popover, PopoverPosition } from "@blueprintjs/core"; import { Theme } from "constants/DefaultTheme"; import { useSelector, useDispatch } from "react-redux"; import { getIsGitConnected } from "../../../../selectors/gitSyncSelectors"; -import getFeatureFlags from "utils/featureFlags"; import { setIsGitSyncModalOpen } from "actions/gitSyncActions"; import { GitSyncModalTab } from "entities/GitSync"; import { Colors } from "constants/Colors"; import { ReactComponent as GitBranch } from "assets/icons/ads/git-branch.svg"; import AnalyticsUtil from "utils/AnalyticsUtil"; +import { selectFeatureFlags } from "selectors/usersSelectors"; const DeployLinkDialog = styled.div` flex-direction: column; @@ -74,6 +74,8 @@ export const DeployLinkButton = withTheme((props: Props) => { const isGitConnected = useSelector(getIsGitConnected); + const featureFlags = useSelector(selectFeatureFlags); + const onClose = () => { setIsOpen(false); }; @@ -96,7 +98,7 @@ export const DeployLinkButton = withTheme((props: Props) => { canEscapeKeyClose={false} content={ - {getFeatureFlags().GIT && !isGitConnected && ( + {featureFlags.GIT && !isGitConnected && ( { +const getBaseOptions = (featureFlags: FeatureFlags) => { + const { JS_EDITOR: isJSEditorEnabled } = featureFlags; if (isJSEditorEnabled) { const jsOption = baseOptions.find( (option: any) => option.value === ActionType.jsFunction, @@ -413,8 +414,9 @@ function getIntegrationOptionsWithChildren( jsActions: Array, createIntegrationOption: TreeDropdownOption, dispatch: any, + featureFlags: FeatureFlags, ) { - const isJSEditorEnabled = getFeatureFlags().JS_EDITOR; + const { JS_EDITOR: isJSEditorEnabled } = featureFlags; const createJSObject: TreeDropdownOption = { label: "New JS Object", value: "JSObject", @@ -527,6 +529,7 @@ function getIntegrationOptionsWithChildren( function useIntegrationsOptionTree() { const pageId = useSelector(getCurrentPageId) || ""; const applicationId = useSelector(getCurrentApplicationId) as string; + const featureFlags = useSelector(selectFeatureFlags); const dispatch = useDispatch(); const plugins = useSelector((state: AppState) => { return state.entities.plugins.list; @@ -539,7 +542,7 @@ function useIntegrationsOptionTree() { pageId, applicationId, pluginGroups, - getBaseOptions(), + getBaseOptions(featureFlags), actions, jsActions, { @@ -557,6 +560,7 @@ function useIntegrationsOptionTree() { }, }, dispatch, + featureFlags, ); } diff --git a/app/client/src/entities/FeatureFlag.ts b/app/client/src/entities/FeatureFlag.ts deleted file mode 100644 index 6eb4a3e6a7..0000000000 --- a/app/client/src/entities/FeatureFlag.ts +++ /dev/null @@ -1,10 +0,0 @@ -type FeatureFlag = { - APP_TEMPLATE: boolean; - JS_EDITOR: boolean; - MULTIPLAYER: boolean; - SNIPPET: boolean; - GIT: boolean; - GIT_IMPORT: boolean; -}; - -export default FeatureFlag; diff --git a/app/client/src/entities/FeatureFlags.ts b/app/client/src/entities/FeatureFlags.ts new file mode 100644 index 0000000000..6a5d8e44c8 --- /dev/null +++ b/app/client/src/entities/FeatureFlags.ts @@ -0,0 +1,10 @@ +type FeatureFlags = { + APP_TEMPLATE?: boolean; + JS_EDITOR?: boolean; + MULTIPLAYER?: boolean; + SNIPPET?: boolean; + GIT?: boolean; + GIT_IMPORT?: boolean; +}; + +export default FeatureFlags; diff --git a/app/client/src/pages/Applications/ImportApplicationModal.tsx b/app/client/src/pages/Applications/ImportApplicationModal.tsx index 220a0f6e0b..060514d734 100644 --- a/app/client/src/pages/Applications/ImportApplicationModal.tsx +++ b/app/client/src/pages/Applications/ImportApplicationModal.tsx @@ -27,7 +27,7 @@ import { getIsImportingApplication } from "selectors/applicationSelectors"; import { ReduxActionTypes } from "constants/ReduxActionConstants"; import Dialog from "../../components/ads/DialogComponent"; import { Classes } from "@blueprintjs/core"; -import getFeatureFlags from "../../utils/featureFlags"; +import { selectFeatureFlags } from "selectors/usersSelectors"; const StyledDialog = styled(Dialog)` && .${Classes.DIALOG_HEADER} { @@ -259,7 +259,9 @@ function ImportApplicationModal(props: ImportApplicationModalProps) { }, [appFileToBeUploaded, importingApplication]); const onRemoveFile = useCallback(() => setAppFileToBeUploaded(null), []); - const isGitImportFeatureEnabled = getFeatureFlags().GIT_IMPORT; + + const featureFlags = useSelector(selectFeatureFlags); + const { GIT_IMPORT: isGitImportFeatureEnabled } = featureFlags; return ( { dispatch(updateApplication(id, data)); }; + const featureFlags = useSelector(selectFeatureFlags); useEffect(() => { // Clears URL params cache @@ -931,7 +931,7 @@ function ApplicationsSection(props: any) { isMobile={isMobile} > {organizationsListComponent} - {getFeatureFlags().GIT_IMPORT && } + {featureFlags.GIT_IMPORT && } ); diff --git a/app/client/src/pages/Editor/EditorAppName/NavigationMenuData.ts b/app/client/src/pages/Editor/EditorAppName/NavigationMenuData.ts index 5898c71403..a4e30ccca7 100644 --- a/app/client/src/pages/Editor/EditorAppName/NavigationMenuData.ts +++ b/app/client/src/pages/Editor/EditorAppName/NavigationMenuData.ts @@ -23,7 +23,6 @@ import { } from "../../Applications/permissionHelpers"; import { getCurrentApplication } from "selectors/applicationSelectors"; import { Colors } from "constants/Colors"; -import getFeatureFlags from "utils/featureFlags"; import { setIsGitSyncModalOpen } from "actions/gitSyncActions"; import { GitSyncModalTab } from "entities/GitSync"; import { getIsGitConnected } from "selectors/gitSyncSelectors"; @@ -38,6 +37,7 @@ import { redoAction, undoAction } from "actions/pageActions"; import { redoShortCut, undoShortCut } from "utils/helpers"; import { pageListEditorURL } from "RouteBuilder"; import AnalyticsUtil from "utils/AnalyticsUtil"; +import { selectFeatureFlags } from "selectors/usersSelectors"; type NavigationMenuDataProps = ThemeProp & { editMode: typeof noop; @@ -122,7 +122,9 @@ export const GetNavigationMenuData = ({ }, ]; - if (getFeatureFlags().GIT && !isGitConnected) { + const featureFlags = useSelector(selectFeatureFlags); + + if (featureFlags.GIT && !isGitConnected) { deployOptions.push({ text: createMessage(CONNECT_TO_GIT_OPTION), onClick: () => openGitConnectionPopup(), diff --git a/app/client/src/pages/Editor/EditorHeader.tsx b/app/client/src/pages/Editor/EditorHeader.tsx index 211160ac8f..61f336814c 100644 --- a/app/client/src/pages/Editor/EditorHeader.tsx +++ b/app/client/src/pages/Editor/EditorHeader.tsx @@ -33,7 +33,7 @@ import { } from "selectors/applicationSelectors"; import EditorAppName from "./EditorAppName"; import ProfileDropdown from "pages/common/ProfileDropdown"; -import { getCurrentUser } from "selectors/usersSelectors"; +import { getCurrentUser, selectFeatureFlags } from "selectors/usersSelectors"; import { ANONYMOUS_USERNAME, User } from "constants/userConstants"; import Button, { Size } from "components/ads/Button"; import Icon, { IconSize } from "components/ads/Icon"; @@ -52,7 +52,6 @@ import { useLocation } from "react-router"; import { showConnectGitModal } from "actions/gitSyncActions"; import RealtimeAppEditors from "./RealtimeAppEditors"; import { EditorSaveIndicator } from "./EditorSaveIndicator"; -import getFeatureFlags from "utils/featureFlags"; import { retryPromise } from "utils/AppsmithUtils"; import { fetchUsersForOrg } from "actions/orgActions"; @@ -298,9 +297,11 @@ export function EditorHeader(props: EditorHeaderProps) { showAppInviteUsersDialogSelector, ); + const featureFlags = useSelector(selectFeatureFlags); + const handleClickDeploy = useCallback( (fromDeploy?: boolean) => { - if (getFeatureFlags().GIT && isGitConnected) { + if (featureFlags.GIT && isGitConnected) { dispatch(showConnectGitModal()); AnalyticsUtil.logEvent("GS_DEPLOY_GIT_CLICK", { source: fromDeploy @@ -311,7 +312,7 @@ export function EditorHeader(props: EditorHeaderProps) { handlePublish(); } }, - [getFeatureFlags().GIT, dispatch, handlePublish], + [featureFlags.GIT, dispatch, handlePublish], ); /** diff --git a/app/client/src/pages/Editor/gitSync/QuickGitActions/index.tsx b/app/client/src/pages/Editor/gitSync/QuickGitActions/index.tsx index 2ade4e6bdd..635006981f 100644 --- a/app/client/src/pages/Editor/gitSync/QuickGitActions/index.tsx +++ b/app/client/src/pages/Editor/gitSync/QuickGitActions/index.tsx @@ -32,7 +32,6 @@ import { showConnectGitModal, } from "actions/gitSyncActions"; import { GitSyncModalTab } from "entities/GitSync"; -import getFeatureFlags from "utils/featureFlags"; import { getCountOfChangesToCommit, getGitStatus, @@ -45,6 +44,7 @@ import SpinnerLoader from "pages/common/SpinnerLoader"; import { inGuidedTour } from "selectors/onboardingSelectors"; import Icon, { IconName, IconSize } from "components/ads/Icon"; import AnalyticsUtil from "utils/AnalyticsUtil"; +import { selectFeatureFlags } from "selectors/usersSelectors"; type QuickActionButtonProps = { className?: string; @@ -229,8 +229,9 @@ const PlaceholderButton = styled.div` function ConnectGitPlaceholder() { const dispatch = useDispatch(); const isInGuidedTour = useSelector(inGuidedTour); + const featureFlags = useSelector(selectFeatureFlags); - const isTooltipEnabled = !getFeatureFlags().GIT || isInGuidedTour; + const isTooltipEnabled = !featureFlags.GIT || isInGuidedTour; const tooltipContent = !isInGuidedTour ? ( <>
{createMessage(NOT_LIVE_FOR_YOU_YET)}
@@ -242,7 +243,7 @@ function ConnectGitPlaceholder() {
{createMessage(DURING_ONBOARDING_TOUR)}
); - const isGitConnectionEnabled = getFeatureFlags().GIT && !isInGuidedTour; + const isGitConnectionEnabled = featureFlags.GIT && !isInGuidedTour; return ( @@ -297,6 +298,7 @@ export default function QuickGitActions() { const isFetchingGitStatus = useSelector(getIsFetchingGitStatus); const showPullLoadingState = isPullInProgress || isFetchingGitStatus; const changesToCommit = useSelector(getCountOfChangesToCommit); + const featureFlags = useSelector(selectFeatureFlags); const quickActionButtons = getQuickActionButtons({ commit: () => { @@ -345,7 +347,7 @@ export default function QuickGitActions() { showPullLoadingState, changesToCommit, }); - return getFeatureFlags().GIT && isGitConnected ? ( + return featureFlags.GIT && isGitConnected ? ( {quickActionButtons.map((button) => ( diff --git a/app/client/src/pages/Settings/WithSuperUserHoc.tsx b/app/client/src/pages/Settings/WithSuperUserHoc.tsx index e07d3bb15f..6925a364eb 100644 --- a/app/client/src/pages/Settings/WithSuperUserHoc.tsx +++ b/app/client/src/pages/Settings/WithSuperUserHoc.tsx @@ -9,7 +9,7 @@ export default function WithSuperUserHOC( ) { return function Wrapped(props: RouteComponentProps) { const user = useSelector(getCurrentUser); - + if (!user) return null; if (!user?.isSuperUser || !user?.isConfigurable) { return ; } diff --git a/app/client/src/pages/UserAuth/requiresAuthHOC.tsx b/app/client/src/pages/UserAuth/requiresAuthHOC.tsx index 6d6f51ab54..bddfc092be 100644 --- a/app/client/src/pages/UserAuth/requiresAuthHOC.tsx +++ b/app/client/src/pages/UserAuth/requiresAuthHOC.tsx @@ -9,7 +9,7 @@ import { APPLICATIONS_URL, AUTH_LOGIN_URL } from "constants/routes"; export const requiresUnauth = (Component: React.ComponentType) => { function Wrapped(props: any) { const user = useSelector(getCurrentUser); - + if (!user) return null; if (user?.email && user?.email !== ANONYMOUS_USERNAME) { return ; } @@ -22,7 +22,7 @@ export const requiresUnauth = (Component: React.ComponentType) => { export const requiresAuth = (Component: React.ComponentType) => { return function Wrapped(props: any) { const user = useSelector(getCurrentUser); - + if (!user) return null; if (user?.email && user?.email !== ANONYMOUS_USERNAME) { return ; } diff --git a/app/client/src/pages/UserProfile/index.tsx b/app/client/src/pages/UserProfile/index.tsx index d4d235dbb3..51b659122e 100644 --- a/app/client/src/pages/UserProfile/index.tsx +++ b/app/client/src/pages/UserProfile/index.tsx @@ -7,10 +7,11 @@ import { Icon } from "@blueprintjs/core"; // import { Link } from "react-router-dom"; import General from "./General"; import { Colors } from "constants/Colors"; -import getFeatureFlags from "utils/featureFlags"; import GitConfig from "./GitConfig"; import { useLocation } from "react-router"; import { GIT_PROFILE_ROUTE } from "constants/routes"; +import { useSelector } from "react-redux"; +import { selectFeatureFlags } from "selectors/usersSelectors"; const ProfileWrapper = styled.div` width: ${(props) => props.theme.pageContentWidth}px; @@ -32,6 +33,7 @@ const LinkToApplications = styled.div` function UserProfile() { const location = useLocation(); + const featureFlags = useSelector(selectFeatureFlags); let initialTabIndex = 0; const tabs: TabProp[] = [ @@ -43,7 +45,7 @@ function UserProfile() { }, ]; - if (getFeatureFlags().GIT) { + if (featureFlags.GIT) { tabs.push({ key: "gitConfig", title: "Git user config", diff --git a/app/client/src/pages/common/PageHeader.tsx b/app/client/src/pages/common/PageHeader.tsx index 9ab700d7ee..26f112f380 100644 --- a/app/client/src/pages/common/PageHeader.tsx +++ b/app/client/src/pages/common/PageHeader.tsx @@ -1,7 +1,7 @@ import React, { useState, useMemo, useEffect } from "react"; import { Link, useLocation } from "react-router-dom"; -import { connect, useDispatch } from "react-redux"; -import { getCurrentUser } from "selectors/usersSelectors"; +import { connect, useDispatch, useSelector } from "react-redux"; +import { getCurrentUser, selectFeatureFlags } from "selectors/usersSelectors"; import styled from "styled-components"; import StyledHeader from "components/designSystems/appsmith/StyledHeader"; import { ReactComponent as AppsmithLogo } from "assets/svg/appsmith_logo_primary.svg"; @@ -28,7 +28,6 @@ import { Indices } from "constants/Layers"; import Icon, { IconSize } from "components/ads/Icon"; import { TemplatesTabItem } from "pages/Templates/TemplatesTabItem"; import { getTemplateNotificationSeenAction } from "actions/templateActions"; -import getFeatureFlags from "utils/featureFlags"; const StyledPageHeader = styled(StyledHeader)<{ hideShadow?: boolean; @@ -131,6 +130,8 @@ export function PageHeader(props: PageHeaderProps) { =${queryParams.get("redirectUrl")}`; } + const featureFlags = useSelector(selectFeatureFlags); + useEffect(() => { dispatch(getTemplateNotificationSeenAction()); }, []); @@ -156,9 +157,9 @@ export function PageHeader(props: PageHeaderProps) { const showTabs = useMemo(() => { return ( tabs.some((tab) => tab.matcher(location.pathname)) && - getFeatureFlags().APP_TEMPLATE + !!featureFlags.APP_TEMPLATE ); - }, [location.pathname]); + }, [featureFlags, location.pathname]); return ( ({ + [ReduxActionTypes.FETCH_FEATURE_FLAGS_SUCCESS]: ( + state: UsersReduxState, + action: ReduxAction, + ) => ({ ...state, - featureFlagFetched: true, + featureFlag: { + data: action.payload, + isFetched: true, + }, }), [ReduxActionErrorTypes.FETCH_FEATURE_FLAGS_ERROR]: ( state: UsersReduxState, ) => ({ ...state, - featureFlagFetched: true, + featureFlag: { + data: {}, + isFetched: true, + }, }), [ReduxActionTypes.UPDATE_USERS_COMMENTS_ONBOARDING_STATE]: ( state: UsersReduxState, @@ -195,7 +208,10 @@ export interface UsersReduxState { currentUser?: User; error: string; propPanePreferences?: PropertyPanePositionConfig; - featureFlagFetched: boolean; + featureFlag: { + isFetched: boolean; + data: FeatureFlags; + }; } export default usersReducer; diff --git a/app/client/src/sagas/PostEvaluationSagas.ts b/app/client/src/sagas/PostEvaluationSagas.ts index b5e3cf79ad..d928334aa1 100644 --- a/app/client/src/sagas/PostEvaluationSagas.ts +++ b/app/client/src/sagas/PostEvaluationSagas.ts @@ -38,6 +38,8 @@ import { getAppMode } from "selectors/applicationSelectors"; import { APP_MODE } from "entities/App"; import { dataTreeTypeDefCreator } from "utils/autocomplete/dataTreeTypeDefCreator"; import TernServer from "utils/autocomplete/TernServer"; +import { selectFeatureFlags } from "selectors/usersSelectors"; +import FeatureFlags from "entities/FeatureFlags"; const getDebuggerErrors = (state: AppState) => state.ui.debugger.errors; /** @@ -301,7 +303,7 @@ export function* logSuccessfulBindings( dataTree: DataTree, evaluationOrder: string[], ) { - const appMode = yield select(getAppMode); + const appMode: APP_MODE = yield select(getAppMode); if (appMode === APP_MODE.PUBLISHED) return; if (!evaluationOrder) return; evaluationOrder.forEach((evaluatedPath) => { @@ -371,8 +373,10 @@ export function* updateTernDefinitions( const treeWithoutPrivateWidgets = getDataTreeWithoutPrivateWidgets( dataTree, ); + const featureFlags: FeatureFlags = yield select(selectFeatureFlags); const { def, entityInfo } = dataTreeTypeDefCreator( treeWithoutPrivateWidgets, + !!featureFlags.JS_EDITOR, ); TernServer.updateDef("DATA_TREE", def, entityInfo); const end = performance.now(); diff --git a/app/client/src/sagas/ReflowSagas.ts b/app/client/src/sagas/ReflowSagas.ts index 6e5dbe1494..03a73100cf 100644 --- a/app/client/src/sagas/ReflowSagas.ts +++ b/app/client/src/sagas/ReflowSagas.ts @@ -3,13 +3,14 @@ import { updateReflowOnBoardingAction, } from "actions/reflowActions"; import { + ReduxAction, ReduxActionErrorTypes, ReduxActionTypes, ReflowReduxActionTypes, } from "constants/ReduxActionConstants"; import { User } from "constants/userConstants"; import { isBoolean } from "lodash"; -import { all, put, select, takeLatest } from "redux-saga/effects"; +import { all, put, select, takeLatest, take } from "redux-saga/effects"; import { getCurrentUser } from "selectors/usersSelectors"; import { getReflowBetaFlag, @@ -20,7 +21,13 @@ import { function* initReflowStates() { try { - const user: User = yield select(getCurrentUser); + let user: User = yield select(getCurrentUser); + if (!user) { + const userFetched: ReduxAction = yield take( + ReduxActionTypes.FETCH_USER_DETAILS_SUCCESS, + ); + user = userFetched.payload; + } const { email } = user; if (email) { const enableReflow: boolean = yield getReflowBetaFlag(email); @@ -62,8 +69,8 @@ function* closeReflowOnboardingCard() { } export default function* reflowSagas() { - yield all([takeLatest(ReduxActionTypes.INITIALIZE_EDITOR, initReflowStates)]); yield all([ + takeLatest(ReduxActionTypes.INITIALIZE_EDITOR, initReflowStates), takeLatest( ReflowReduxActionTypes.CLOSE_ONBOARDING_CARD, closeReflowOnboardingCard, diff --git a/app/client/src/sagas/index.tsx b/app/client/src/sagas/index.tsx index 53b3629f5f..9bee23ec1d 100644 --- a/app/client/src/sagas/index.tsx +++ b/app/client/src/sagas/index.tsx @@ -1,4 +1,4 @@ -import { call, all, spawn } from "redux-saga/effects"; +import { call, all, spawn, race, take } from "redux-saga/effects"; import pageSagas from "sagas/PageSagas"; import { watchActionSagas } from "./ActionSagas"; import { watchJSActionSagas } from "./JSActionSagas"; @@ -45,6 +45,7 @@ import * as sentry from "@sentry/react"; import formEvaluationChangeListener from "./FormEvaluationSaga"; import SuperUserSagas from "./SuperUserSagas"; import reflowSagas from "./ReflowSagas"; +import { ReduxActionTypes } from "constants/ReduxActionConstants"; const sagas = [ initSagas, @@ -92,20 +93,26 @@ const sagas = [ reflowSagas, ]; -export function* rootSaga(sagasToRun = sagas) { - yield all( - sagasToRun.map((saga) => - spawn(function*() { - while (true) { - try { - yield call(saga); - break; - } catch (e) { - log.error(e); - sentry.captureException(e); +export function* rootSaga(sagasToRun = sagas): any { + // This race effect ensures that we fail as soon as the first safe crash is dispatched. + // Without this, all the subsequent safe crash failures would be shown in the toast messages as well. + const result = yield race({ + running: all( + sagasToRun.map((saga) => + spawn(function*() { + while (true) { + try { + yield call(saga); + break; + } catch (e) { + log.error(e); + sentry.captureException(e); + } } - } - }), + }), + ), ), - ); + crashed: take(ReduxActionTypes.SAFE_CRASH_APPSMITH), + }); + if (result.crashed) yield call(rootSaga); } diff --git a/app/client/src/sagas/userSagas.tsx b/app/client/src/sagas/userSagas.tsx index c84970f3af..2c6698ab34 100644 --- a/app/client/src/sagas/userSagas.tsx +++ b/app/client/src/sagas/userSagas.tsx @@ -15,12 +15,7 @@ import UserApi, { UpdateUserRequest, LeaveOrgRequest, } from "api/UserApi"; -import { - APPLICATIONS_URL, - AUTH_LOGIN_URL, - BASE_URL, - SETUP, -} from "constants/routes"; +import { AUTH_LOGIN_URL, SETUP } from "constants/routes"; import history from "utils/history"; import { ApiResponse } from "api/ApiResponses"; import { @@ -37,7 +32,6 @@ import { invitedUserSignupSuccess, fetchFeatureFlagsSuccess, fetchFeatureFlagsError, - fetchFeatureFlagsInit, } from "actions/userActions"; import AnalyticsUtil from "utils/AnalyticsUtil"; import { INVITE_USERS_TO_ORG_FORM } from "constants/forms"; @@ -125,11 +119,6 @@ export function* getCurrentUserSaga() { response.data.username !== ANONYMOUS_USERNAME ) { enableTelemetry && AnalyticsUtil.identifyUser(response.data); - // make fetch feature call only if logged in - yield put(fetchFeatureFlagsInit()); - } else { - // reset the flagsFetched flag - yield put(fetchFeatureFlagsSuccess()); } yield put({ type: ReduxActionTypes.FETCH_USER_DETAILS_SUCCESS, @@ -137,12 +126,6 @@ export function* getCurrentUserSaga() { }); if (response.data.emptyInstance) { history.replace(SETUP); - } else if (window.location.pathname === BASE_URL) { - if (response.data.isAnonymous) { - history.replace(AUTH_LOGIN_URL); - } else { - history.replace(APPLICATIONS_URL); - } } PerformanceTracker.stopAsyncTracking( PerformanceTransactionName.USER_ME_API, @@ -161,7 +144,7 @@ export function* getCurrentUserSaga() { }); yield put({ - type: ReduxActionTypes.SAFE_CRASH_APPSMITH, + type: ReduxActionTypes.SAFE_CRASH_APPSMITH_REQUEST, payload: { code: ERROR_CODES.SERVER_ERROR, }, @@ -441,8 +424,7 @@ function* fetchFeatureFlags() { const response: ApiResponse = yield call(UserApi.fetchFeatureFlags); const isValidResponse: boolean = yield validateResponse(response); if (isValidResponse) { - (window as any).FEATURE_FLAGS = response.data; - yield put(fetchFeatureFlagsSuccess()); + yield put(fetchFeatureFlagsSuccess(response.data)); } } catch (error) { log.error(error); diff --git a/app/client/src/selectors/appCollabSelectors.tsx b/app/client/src/selectors/appCollabSelectors.tsx index cd81d899a9..e3a1d956c6 100644 --- a/app/client/src/selectors/appCollabSelectors.tsx +++ b/app/client/src/selectors/appCollabSelectors.tsx @@ -1,8 +1,7 @@ import { createSelector } from "reselect"; import { AppState } from "reducers"; import { AppCollabReducerState } from "reducers/uiReducers/appCollabReducer"; -import { getCurrentUser } from "./usersSelectors"; -import getFeatureFlags from "../utils/featureFlags"; +import { getCurrentUser, selectFeatureFlags } from "./usersSelectors"; import { User } from "entities/AppCollab/CollabInterfaces"; import { ANONYMOUS_USERNAME } from "constants/userConstants"; @@ -15,7 +14,10 @@ export const getRealtimeAppEditors = createSelector( appCollab.editors.filter((el) => el.email !== currentUser?.email), ); -export const isMultiplayerEnabledForUser = () => getFeatureFlags().MULTIPLAYER; +export const isMultiplayerEnabledForUser = createSelector( + selectFeatureFlags, + (featureFlags) => featureFlags.MULTIPLAYER, +); export const getConcurrentPageEditors = (state: AppState) => state.ui.appCollab.pageEditors; diff --git a/app/client/src/selectors/entitiesSelector.ts b/app/client/src/selectors/entitiesSelector.ts index a82c75f8ff..bd86509c77 100644 --- a/app/client/src/selectors/entitiesSelector.ts +++ b/app/client/src/selectors/entitiesSelector.ts @@ -21,9 +21,9 @@ import { JSCollectionDataState } from "reducers/entityReducers/jsActionsReducer" import { JSCollection } from "entities/JSCollection"; import { DefaultPlugin, GenerateCRUDEnabledPluginMap } from "../api/PluginApi"; import { APP_MODE } from "entities/App"; -import getFeatureFlags from "utils/featureFlags"; import { ExplorerFileEntity } from "pages/Editor/Explorer/helpers"; import { ActionValidationConfigMap } from "constants/PropertyControlConstants"; +import { selectFeatureFlags } from "./usersSelectors"; export const getEntities = (state: AppState): AppState["entities"] => state.entities; @@ -663,8 +663,9 @@ export const selectFilesForExplorer = createSelector( getActionsForCurrentPage, getJSCollectionsForCurrentPage, selectDatasourceIdToNameMap, - (actions, jsActions, datasourceIdToNameMap) => { - const isJSEditorEnabled = getFeatureFlags().JS_EDITOR; + selectFeatureFlags, + (actions, jsActions, datasourceIdToNameMap, featureFlags) => { + const { JS_EDITOR: isJSEditorEnabled } = featureFlags; const files = [...actions, ...(isJSEditorEnabled ? jsActions : [])].reduce( (acc, file) => { let group = ""; diff --git a/app/client/src/selectors/usersSelectors.tsx b/app/client/src/selectors/usersSelectors.tsx index 09f70828d7..0753a4881f 100644 --- a/app/client/src/selectors/usersSelectors.tsx +++ b/app/client/src/selectors/usersSelectors.tsx @@ -11,4 +11,7 @@ export const getProppanePreference = ( state: AppState, ): PropertyPanePositionConfig | undefined => state.ui.users.propPanePreferences; export const getFeatureFlagsFetched = (state: AppState) => - state.ui.users.featureFlagFetched; + state.ui.users.featureFlag.isFetched; + +export const selectFeatureFlags = (state: AppState) => + state.ui.users.featureFlag.data; diff --git a/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.ts b/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.ts index 5537f7a52b..e99b87d31c 100644 --- a/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.ts +++ b/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.ts @@ -15,7 +15,6 @@ import { isWidget, } from "workers/evaluationUtils"; import { DataTreeDefEntityInformation } from "utils/autocomplete/TernServer"; -import getFeatureFlags from "utils/featureFlags"; // When there is a complex data type, we store it in extra def and refer to it // in the def let extraDefs: any = {}; @@ -27,12 +26,12 @@ let extraDefs: any = {}; // or DATA_TREE.ACTION.ACTION.Api1 export const dataTreeTypeDefCreator = ( dataTree: DataTree, + isJSEditorEnabled: boolean, ): { def: Def; entityInfo: Map } => { const def: any = { "!name": "DATA_TREE", }; const entityMap: Map = new Map(); - const isJSEditorEnabled = getFeatureFlags().JS_EDITOR; Object.entries(dataTree).forEach(([entityName, entity]) => { if (isWidget(entity)) { const widgetType = entity.type; diff --git a/app/client/src/utils/featureFlags.ts b/app/client/src/utils/featureFlags.ts deleted file mode 100644 index 32224087f1..0000000000 --- a/app/client/src/utils/featureFlags.ts +++ /dev/null @@ -1,5 +0,0 @@ -import FeatureFlag from "entities/FeatureFlag"; - -export default function getFeatureFlags(): FeatureFlag { - return (window as any).FEATURE_FLAGS || {}; -} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/SecurityConfig.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/SecurityConfig.java index 129bb9e251..5eb92e7d96 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/SecurityConfig.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/SecurityConfig.java @@ -121,6 +121,7 @@ public class SecurityConfig { ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, USER_URL + "/invite/verify"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.PUT, USER_URL + "/invite/confirm"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, USER_URL + "/me"), + ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, USER_URL + "/features"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, ACTION_URL + "/**"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, ACTION_COLLECTION_URL + "/view"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, PAGE_URL + "/**"), diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/FeatureFlagServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/FeatureFlagServiceCEImpl.java index eff4f064ef..b0bc4813da 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/FeatureFlagServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/FeatureFlagServiceCEImpl.java @@ -52,6 +52,10 @@ public class FeatureFlagServiceCEImpl implements FeatureFlagServiceCE { .flatMap(featureName -> Mono.just(featureName).zipWith(currentUser)); return featureUserTuple - .collectMap(tuple -> tuple.getT1(), tuple -> check(tuple.getT1(), tuple.getT2())); + .filter(objects -> !objects.getT2().isAnonymous()) + .collectMap( + Tuple2::getT1, + tuple -> check(tuple.getT1(), tuple.getT2()) + ); } }