chore: Parallelise feature flags and user info API (#12263)

This commit is contained in:
arunvjn 2022-04-07 23:27:32 +05:30 committed by GitHub
parent 461f35380b
commit fabfb65a4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 153 additions and 113 deletions

View File

@ -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 (
<Router history={history}>
<Suspense fallback={loadingIndicator}>
@ -128,14 +129,13 @@ function AppRouter(props: {
exact
path={SIGNUP_SUCCESS_URL}
/>
<SentryRoute component={UserProfile} path={PROFILE} />
<SentryRoute
component={UnsubscribeEmail}
path={UNSUBSCRIBE_EMAIL_URL}
/>
<SentryRoute component={Setup} exact path={SETUP} />
{getFeatureFlags().APP_TEMPLATE && (
{featureFlags.APP_TEMPLATE && (
<SentryRoute
component={TemplatesListLoader}
path={TEMPLATES_PATH}
@ -174,7 +174,7 @@ const mapStateToProps = (state: AppState) => ({
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);

View File

@ -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) => ({

View File

@ -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={
<DeployLinkDialog>
{getFeatureFlags().GIT && !isGitConnected && (
{featureFlags.GIT && !isGitConnected && (
<DeployLink
className="t--connect-to-git-btn"
onClick={goToGitConnectionPopup}

View File

@ -36,7 +36,6 @@ import { DataTree, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
import { getEntityNameAndPropertyPath } from "workers/evaluationUtils";
import { JSCollectionData } from "reducers/entityReducers/jsActionsReducer";
import { createNewJSCollection } from "actions/jsPaneActions";
import getFeatureFlags from "utils/featureFlags";
import { JSAction, Variable } from "entities/JSCollection";
import {
CLEAR_INTERVAL,
@ -60,10 +59,11 @@ import {
import { toggleShowGlobalSearchModal } from "actions/globalSearchActions";
import { filterCategories, SEARCH_CATEGORY_ID } from "../GlobalSearch/utils";
import { ActionDataState } from "reducers/entityReducers/actionsReducer";
import { selectFeatureFlags } from "selectors/usersSelectors";
import FeatureFlags from "entities/FeatureFlags";
/* eslint-disable @typescript-eslint/ban-types */
/* TODO: Function and object types need to be updated to enable the lint rule */
const isJSEditorEnabled = getFeatureFlags().JS_EDITOR;
const baseOptions: { label: string; value: string }[] = [
{
label: createMessage(NO_ACTION),
@ -127,7 +127,8 @@ const baseOptions: { label: string; value: string }[] = [
},
];
const getBaseOptions = () => {
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<JSCollectionData>,
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,
);
}

View File

@ -1,10 +0,0 @@
type FeatureFlag = {
APP_TEMPLATE: boolean;
JS_EDITOR: boolean;
MULTIPLAYER: boolean;
SNIPPET: boolean;
GIT: boolean;
GIT_IMPORT: boolean;
};
export default FeatureFlag;

View File

@ -0,0 +1,10 @@
type FeatureFlags = {
APP_TEMPLATE?: boolean;
JS_EDITOR?: boolean;
MULTIPLAYER?: boolean;
SNIPPET?: boolean;
GIT?: boolean;
GIT_IMPORT?: boolean;
};
export default FeatureFlags;

View File

@ -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 (
<StyledDialog

View File

@ -40,7 +40,7 @@ import { isPermitted, PERMISSION_TYPE } from "./permissionHelpers";
import FormDialogComponent from "components/editorComponents/form/FormDialogComponent";
import Dialog from "components/ads/DialogComponent";
import { User } from "constants/userConstants";
import { getCurrentUser } from "selectors/usersSelectors";
import { getCurrentUser, selectFeatureFlags } from "selectors/usersSelectors";
import { CREATE_ORGANIZATION_FORM_NAME } from "constants/forms";
import {
DropdownOnSelectActions,
@ -85,7 +85,6 @@ import {
import { ReactComponent as NoAppsFoundIcon } from "assets/svg/no-apps-icon.svg";
import { setHeaderMeta } from "actions/themeActions";
import getFeatureFlags from "utils/featureFlags";
import SharedUserList from "pages/common/SharedUserList";
import { useIsMobileDevice } from "utils/hooks/useDeviceDetect";
import { Indices } from "constants/Layers";
@ -523,6 +522,7 @@ function ApplicationsSection(props: any) {
) => {
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 && <GitSyncModal isImport />}
{featureFlags.GIT_IMPORT && <GitSyncModal isImport />}
<ReconnectDatasourceModal />
</ApplicationContainer>
);

View File

@ -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(),

View File

@ -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],
);
/**

View File

@ -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 ? (
<>
<div>{createMessage(NOT_LIVE_FOR_YOU_YET)}</div>
@ -242,7 +243,7 @@ function ConnectGitPlaceholder() {
<div>{createMessage(DURING_ONBOARDING_TOUR)}</div>
</>
);
const isGitConnectionEnabled = getFeatureFlags().GIT && !isInGuidedTour;
const isGitConnectionEnabled = featureFlags.GIT && !isInGuidedTour;
return (
<Container>
@ -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 ? (
<Container>
<BranchButton />
{quickActionButtons.map((button) => (

View File

@ -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 <Redirect to={APPLICATIONS_URL} />;
}

View File

@ -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 <Redirect to={APPLICATIONS_URL} />;
}
@ -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 <Component {...props} />;
}

View File

@ -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",

View File

@ -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 (
<StyledPageHeader

View File

@ -11,6 +11,7 @@ import {
DefaultCurrentUserDetails,
User,
} from "constants/userConstants";
import FeatureFlags from "entities/FeatureFlags";
const initialState: UsersReduxState = {
loadingStates: {
@ -22,7 +23,10 @@ const initialState: UsersReduxState = {
error: "",
current: undefined,
currentUser: undefined,
featureFlagFetched: false,
featureFlag: {
data: {},
isFetched: false,
},
};
const usersReducer = createReducer(initialState, {
@ -155,15 +159,24 @@ const usersReducer = createReducer(initialState, {
photoId: action.payload.photoId,
},
}),
[ReduxActionTypes.FETCH_FEATURE_FLAGS_SUCCESS]: (state: UsersReduxState) => ({
[ReduxActionTypes.FETCH_FEATURE_FLAGS_SUCCESS]: (
state: UsersReduxState,
action: ReduxAction<FeatureFlags>,
) => ({
...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;

View File

@ -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();

View File

@ -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<User> = 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,

View File

@ -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);
}

View File

@ -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);

View File

@ -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;

View File

@ -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 = "";

View File

@ -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;

View File

@ -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<string, DataTreeDefEntityInformation> } => {
const def: any = {
"!name": "DATA_TREE",
};
const entityMap: Map<string, DataTreeDefEntityInformation> = new Map();
const isJSEditorEnabled = getFeatureFlags().JS_EDITOR;
Object.entries(dataTree).forEach(([entityName, entity]) => {
if (isWidget(entity)) {
const widgetType = entity.type;

View File

@ -1,5 +0,0 @@
import FeatureFlag from "entities/FeatureFlag";
export default function getFeatureFlags(): FeatureFlag {
return (window as any).FEATURE_FLAGS || {};
}

View File

@ -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 + "/**"),

View File

@ -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())
);
}
}