Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor feature flags #1100

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist/
node_modules/
6 changes: 5 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"depConstraints": [
{
"sourceTag": "scope:server",
"onlyDependOnLibsWithTags": ["scope:server", "scope:type-only", "scope:any"]
"onlyDependOnLibsWithTags": ["scope:server", "scope:type-only", "scope:shared", "scope:any"]
},
{
"sourceTag": "scope:worker",
Expand All @@ -24,6 +24,10 @@
"sourceTag": "scope:allow-worker-import",
"onlyDependOnLibsWithTags": ["scope:allow-worker-import", "scope:type-only", "scope:any"]
},
{
"sourceTag": "scope:browser",
"onlyDependOnLibsWithTags": ["scope:browser", "scope:type-only", "scope:shared", "scope:any"]
},
{
"sourceTag": "*",
"onlyDependOnLibsWithTags": ["*"]
Expand Down
30 changes: 27 additions & 3 deletions apps/api/src/app/db/user.db.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getExceptionLog, logger, prisma } from '@jetstream/api-config';
import { UserProfileSession } from '@jetstream/auth/types';
import { FeatureFlags, UserProfileSchema, UserProfileUi } from '@jetstream/types';
import { Prisma, User } from '@prisma/client';

const userSelect: Prisma.UserSelect = {
Expand All @@ -13,6 +14,7 @@ const userSelect: Prisma.UserSelect = {
preferences: {
select: {
skipFrontdoorLogin: true,
featureFlags: true,
},
},
updatedAt: true,
Expand Down Expand Up @@ -65,7 +67,12 @@ const UserFacingProfileSelect = Prisma.validator<Prisma.UserSelect>()({
email: true,
emailVerified: true,
picture: true,
preferences: true,
preferences: {
select: {
skipFrontdoorLogin: true,
featureFlags: true,
},
},
});

export async function findUserWithIdentitiesById(id: string) {
Expand All @@ -79,8 +86,25 @@ export const findIdByUserId = ({ userId }: { userId: string }) => {
return prisma.user.findFirstOrThrow({ where: { userId }, select: { id: true } }).then(({ id }) => id);
};

export const findIdByUserIdUserFacing = ({ userId }: { userId: string }) => {
return prisma.user.findFirstOrThrow({ where: { id: userId }, select: UserFacingProfileSelect }).then(({ id }) => id);
export const findIdByUserIdUserFacing = async ({ userId }: { userId: string }): Promise<UserProfileUi> => {
const user = await prisma.user.findFirstOrThrow({ where: { id: userId }, select: UserFacingProfileSelect });
return UserProfileSchema.parse(user);
};

export const hasFeatureFlagAccess = async ({ userId, flagName }: { userId: string; flagName: keyof FeatureFlags }): Promise<boolean> => {
return prisma.userPreference
.findUnique({
select: { id: true },
where: {
userId,
featureFlags: {
path: [flagName],
equals: true,
},
},
})
.then((result) => !!result?.id)
.catch(() => false);
};

export async function updateUser(
Expand Down
1 change: 0 additions & 1 deletion apps/api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,6 @@ try {
update: {},
where: { id: user.id },
});
logger.info('Example user created');
}
})();
} catch (ex) {
Expand Down
16 changes: 3 additions & 13 deletions apps/jetstream-web-extension/src/pages/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ initAndRenderReact(
</AppWrapper>
);

const featureFlags = new Set<string>();
const pageUrl = new URL(location.href);
const searchParams = pageUrl.searchParams;
const url = searchParams.get('url');
Expand Down Expand Up @@ -57,15 +56,7 @@ export function App() {

return (
<div>
<HeaderNavbar
userProfile={undefined}
featureFlags={featureFlags}
isChromeExtension
// unavailableRoutes={unavailableRoutesDefault}
// orgsDropdown={<OrgPreview selectedOrg={selectedOrg} />}
// userProfile={userProfile}
// featureFlags={featureFlags}
/>
<HeaderNavbar userProfile={undefined} isChromeExtension />
<div className="app-container slds-p-horizontal_xx-small slds-p-vertical_xx-small" data-testid="content">
<Suspense fallback={<AppLoading />}>
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
Expand All @@ -78,8 +69,8 @@ export function App() {
</Route>

<Route path="*" element={<Navigate to={APP_ROUTES.HOME.ROUTE} />} />
<Route path={APP_ROUTES.LOAD.ROUTE} element={<LoadRecords featureFlags={featureFlags} />} />
<Route path={APP_ROUTES.LOAD_MULTIPLE.ROUTE} element={<LoadRecordsMultiObject featureFlags={featureFlags} />} />
<Route path={APP_ROUTES.LOAD.ROUTE} element={<LoadRecords />} />
<Route path={APP_ROUTES.LOAD_MULTIPLE.ROUTE} element={<LoadRecordsMultiObject />} />
<Route path={APP_ROUTES.AUTOMATION_CONTROL.ROUTE} element={<AutomationControl />}>
<Route index element={<AutomationControlSelection />} />
<Route path="editor" element={<AutomationControlEditor />} />
Expand Down Expand Up @@ -112,7 +103,6 @@ export function App() {
<Route path={APP_ROUTES.PLATFORM_EVENT_MONITOR.ROUTE} element={<PlatformEventMonitor />} />
<Route path={APP_ROUTES.OBJECT_EXPORT.ROUTE} element={<SObjectExport />} />
</Routes>
{/* <Route path={APP_ROUTES.SETTINGS.ROUTE} element={<Settings featureFlags={featureFlags} userProfile={userProfile} />} /> */}
</ErrorBoundary>
</Suspense>
</div>
Expand Down
12 changes: 3 additions & 9 deletions apps/jetstream/src/app/AppRoutes.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Maybe, UserProfileUi } from '@jetstream/types';
import { APP_ROUTES, AppHome, OrgSelectionRequired } from '@jetstream/ui-core';
import { useEffect } from 'react';
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
Expand Down Expand Up @@ -80,12 +79,7 @@ const Feedback = lazy(() => import('./components/feedback/Feedback'));

const Settings = lazy(() => import('./components/settings/Settings'));

export interface AppRoutesProps {
featureFlags: Set<string>;
userProfile: Maybe<UserProfileUi>;
}

export const AppRoutes = ({ featureFlags, userProfile }: AppRoutesProps) => {
export const AppRoutes = () => {
const location = useLocation();

// Preload sub-pages if user is on parent page
Expand Down Expand Up @@ -127,15 +121,15 @@ export const AppRoutes = ({ featureFlags, userProfile }: AppRoutesProps) => {
path={APP_ROUTES.LOAD.ROUTE}
element={
<OrgSelectionRequired>
<LoadRecords featureFlags={featureFlags} />
<LoadRecords />
</OrgSelectionRequired>
}
/>
<Route
path={APP_ROUTES.LOAD_MULTIPLE.ROUTE}
element={
<OrgSelectionRequired>
<LoadRecordsMultiObject featureFlags={featureFlags} />
<LoadRecordsMultiObject />
</OrgSelectionRequired>
}
/>
Expand Down
7 changes: 3 additions & 4 deletions apps/jetstream/src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import NotificationsRequestModal from './components/core/NotificationsRequestMod

export const App = () => {
const [userProfile, setUserProfile] = useState<Maybe<UserProfileUi>>();
const [featureFlags, setFeatureFlags] = useState<Set<string>>(new Set(['all']));
const [announcements, setAnnouncements] = useState<Announcement[]>([]);

return (
Expand All @@ -32,17 +31,17 @@ export const App = () => {
<AppStateResetOnOrgChange />
<AppToast />
<LogInitializer />
<NotificationsRequestModal featureFlags={featureFlags} loadDelay={10000} />
<NotificationsRequestModal loadDelay={10000} />
<DownloadFileStream />
<div>
<div data-testid="header">
<HeaderNavbar userProfile={userProfile} featureFlags={featureFlags} />
<HeaderNavbar userProfile={userProfile} />
</div>
<div className="app-container slds-p-horizontal_xx-small slds-p-vertical_xx-small" data-testid="content">
<AnnouncementAlerts announcements={announcements} />
<Suspense fallback={<AppLoading />}>
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
<AppRoutes featureFlags={featureFlags} userProfile={userProfile} />
<AppRoutes />
</ErrorBoundary>
</Suspense>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { ANALYTICS_KEYS, FEATURE_FLAGS } from '@jetstream/shared/constants';
import { hasFeatureFlagAccess } from '@jetstream/shared/ui-utils';
import { ANALYTICS_KEYS } from '@jetstream/shared/constants';
import { DockedComposer, DockedComposerRef } from '@jetstream/ui';
import { useUserPreferenceState } from '@jetstream/ui-core';
import { useAmplitude, useUserPreferenceState } from '@jetstream/ui-core';
import { Fragment, FunctionComponent, useEffect, useRef, useState } from 'react';
import { useAmplitude } from '@jetstream/ui-core';
import NotificationExampleImage from './jetstream-sample-notification.png';

export interface NotificationsRequestModalProps {
featureFlags: Set<string>;
loadDelay?: number;
/** Allow permission modal to be opened even if initially denied */
userInitiated?: boolean;
Expand All @@ -20,7 +17,6 @@ export interface NotificationsRequestModalProps {
* to choose the filename upfront, then we can use it later
*/
export const NotificationsRequestModal: FunctionComponent<NotificationsRequestModalProps> = ({
featureFlags,
loadDelay = 0,
userInitiated = false,
onClose,
Expand All @@ -34,14 +30,12 @@ export const NotificationsRequestModal: FunctionComponent<NotificationsRequestMo
useEffect(() => {
if (window.Notification) {
if (userInitiated || (!userPreferences.deniedNotifications && window.Notification.permission === 'default')) {
if (hasFeatureFlagAccess(featureFlags, FEATURE_FLAGS.NOTIFICATIONS)) {
setTimeout(() => setIsDismissed(false), loadDelay);
trackEvent(ANALYTICS_KEYS.notifications_modal_opened, { userInitiated, currentPermission: window.Notification.permission });
}
setTimeout(() => setIsDismissed(false), loadDelay);
trackEvent(ANALYTICS_KEYS.notifications_modal_opened, { userInitiated, currentPermission: window.Notification.permission });
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loadDelay, featureFlags]);
}, [loadDelay]);

async function handlePermissionRequest(permission: NotificationPermission) {
if (composerRef.current) {
Expand Down
6 changes: 3 additions & 3 deletions libs/auth/server/src/lib/auth.db.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from '@jetstream/auth/types';
import { decryptString, encryptString } from '@jetstream/shared/node-utils';
import { getErrorMessageAndStackObj } from '@jetstream/shared/utils';
import { Maybe } from '@jetstream/types';
import { FeatureFlagsSchema, Maybe } from '@jetstream/types';
import { Prisma } from '@prisma/client';
import { addDays, startOfDay } from 'date-fns';
import { addMinutes } from 'date-fns/addMinutes';
Expand Down Expand Up @@ -608,7 +608,7 @@ async function createUserFromProvider(providerUser: ProviderUser, provider: Oaut
emailVerified: providerUser.emailVerified,
// picture: providerUser.picture,
lastLoggedIn: new Date(),
preferences: { create: { skipFrontdoorLogin: false } },
preferences: { create: { skipFrontdoorLogin: false, featureFlags: FeatureFlagsSchema.parse({}) } },
identities: {
create: {
type: 'oauth',
Expand Down Expand Up @@ -736,7 +736,7 @@ async function createUserFromUserInfo(email: string, name: string, password: str
password: passwordHash,
passwordUpdatedAt: new Date(),
lastLoggedIn: new Date(),
preferences: { create: { skipFrontdoorLogin: false } },
preferences: { create: { skipFrontdoorLogin: false, featureFlags: FeatureFlagsSchema.parse({}) } },
authFactors: {
create: {
type: '2fa-email',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
fireToast,
} from '@jetstream/ui';
import { applicationCookieState, selectedOrgState, selectedOrgType, useAmplitude } from '@jetstream/ui-core';
import { ChangeEvent, FunctionComponent, useEffect, useMemo, useRef, useState } from 'react';
import { ChangeEvent, useEffect, useMemo, useRef, useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import * as XLSX from 'xlsx';
import LoadRecordsMultiObjectErrors from './LoadRecordsMultiObjectErrors';
Expand All @@ -36,11 +36,7 @@ initXlsx(XLSX);
const TEMPLATE_DOWNLOAD_LINK = '/assets/content/Jetstream%20-%20Load%20Records%20to%20Multiple%20Objects%20-%20Template.xlsx';
const HEIGHT_BUFFER = 170;

export interface LoadRecordsMultiObjectProps {
featureFlags: Set<string>;
}

export const LoadRecordsMultiObject: FunctionComponent<LoadRecordsMultiObjectProps> = ({ featureFlags }) => {
export const LoadRecordsMultiObject = () => {
useTitle(TITLES.LOAD);
const isMounted = useRef(true);
const { trackEvent } = useAmplitude();
Expand Down
9 changes: 2 additions & 7 deletions libs/features/load-records/src/LoadRecords.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
useAmplitude,
} from '@jetstream/ui-core';
import startCase from 'lodash/startCase';
import { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useRecoilState, useRecoilValue, useResetRecoilState, useSetRecoilState } from 'recoil';
import LoadRecordsDataPreview from './components/LoadRecordsDataPreview';
import LoadRecordsProgress from './components/LoadRecordsProgress';
Expand All @@ -52,11 +52,7 @@ const steps: Step[] = [
const enabledSteps: Step[] = steps.filter((step) => step.enabled);
const finalStep: Step = enabledSteps[enabledSteps.length - 1];

export interface LoadRecordsProps {
featureFlags: Set<string>;
}

export const LoadRecords: FunctionComponent<LoadRecordsProps> = ({ featureFlags }) => {
export const LoadRecords = () => {
useTitle(TITLES.LOAD);
const isMounted = useRef(true);
const { trackEvent } = useAmplitude();
Expand Down Expand Up @@ -456,7 +452,6 @@ export const LoadRecords: FunctionComponent<LoadRecordsProps> = ({ featureFlags
{currentStep.name === 'sobjectAndFile' && (
<LoadRecordsSelectObjectAndFile
googleApiConfig={googleApiConfig}
featureFlags={featureFlags}
selectedOrg={selectedOrg}
sobjects={sobjects}
selectedSObject={selectedSObject}
Expand Down
2 changes: 0 additions & 2 deletions libs/features/load-records/src/steps/SelectObjectAndFile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import LoadRecordsLoadTypeButtons from '../components/LoadRecordsLoadTypeButtons

export interface LoadRecordsSelectObjectAndFileProps {
googleApiConfig: GoogleApiClientConfig;
featureFlags: Set<string>;
selectedOrg: SalesforceOrgUi;
sobjects: Maybe<DescribeGlobalSObjectResult[]>;
selectedSObject: Maybe<DescribeGlobalSObjectResult>;
Expand Down Expand Up @@ -58,7 +57,6 @@ const onParsedMultipleWorkbooks = async (worksheets: string[]): Promise<string>

export const LoadRecordsSelectObjectAndFile: FunctionComponent<LoadRecordsSelectObjectAndFileProps> = ({
googleApiConfig,
featureFlags,
selectedOrg,
selectedSObject,
isCustomMetadataObject,
Expand Down
18 changes: 2 additions & 16 deletions libs/shared/constants/src/lib/shared-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,6 @@ export const SESSION_EXP_DAYS = 2;
export const SFDC_BULK_API_NULL_VALUE = '#N/A';
export const SFDC_BLANK_PICKLIST_VALUE = '--None--';

export const FEATURE_FLAGS = Object.freeze({
ALL: 'all',
AUTOMATION_CONTROL: 'automation-control',
AUTOMATION_CONTROL_NEW: 'automation-control-new',
QUERY: 'query',
LOAD: 'load',
LOAD_MULTI_OBJ: 'load-multi-object',
PERMISSION_MANAGER: 'permission-manager',
DEPLOYMENT: 'deployment',
NOTIFICATIONS: 'notifications',
ALLOW_GOOGLE_UPLOAD: 'allow-google-upload',
UPDATE_RECORDS: 'update-records',
});

export const INPUT_ACCEPT_FILETYPES: {
ZIP: InputAcceptTypeZip;
CSV: InputAcceptTypeCsv;
Expand Down Expand Up @@ -128,13 +114,13 @@ export const fileExtToGoogleDriveMimeType = {

export const INDEXED_DB = {
KEYS: {
automationControlHistory: 'AUTOMATION:QUERY',
// automationControlHistory: 'AUTOMATION:QUERY',
queryHistory: 'HISTORY:QUERY',
loadSavedMapping: 'LOAD:SAVED_MAPPING',
apexHistory: 'HISTORY:APEX',
deployHistory: 'HISTORY:DEPLOY',
salesforceApiHistory: 'HISTORY:SALESFORCE_API',
recordHistory: 'HISTORY:RECORDS',
// recordHistory: 'HISTORY:RECORDS',
httpCache: 'HTTP:CACHE',
userPreferences: 'USER:PREFERENCES',
sobjectExportSelection: 'USER:SOBJECT_EXPORT_OPTIONS',
Expand Down
Loading
Loading