diff --git a/frontend/api/index.js b/frontend/api/index.js index 6302af8af8b6..c36666d4b2d8 100755 --- a/frontend/api/index.js +++ b/frontend/api/index.js @@ -49,6 +49,7 @@ app.get('/config/project-overrides', (req, res) => { */ const values = [ + { name: 'isFlagsmithOnFlagsmith', value: !!process.env.FLAGSMITH_ON_FLAGSMITH_API_KEY && !!process.env.FLAGSMITH_ON_FLAGSMITH_API_URL }, { name: 'preventSignup', value: envToBool('PREVENT_SIGNUP', false) }, { name: 'preventEmailPassword', diff --git a/frontend/common/stores/base/_store.js b/frontend/common/stores/base/_store.js index 1f17ec97fff2..34c2e38d9ac5 100644 --- a/frontend/common/stores/base/_store.js +++ b/frontend/common/stores/base/_store.js @@ -18,6 +18,7 @@ module.exports = Object.assign({}, EventEmitter.prototype, { // console.log('change', this.id) this.trigger(DEFAULT_CHANGE_EVENT) }, + // Config store uses {type: FlagsmithStartupErrors, message: string} error: null, goneABitWest() { this.hasLoaded = true @@ -32,9 +33,9 @@ module.exports = Object.assign({}, EventEmitter.prototype, { isSaving: false, - loaded() { + loaded(persistError = false) { this.hasLoaded = true - this.error = null + this.error = persistError ? this.error : null this.isLoading = false this.trigger(DEFAULT_LOADED_EVENT) this.trigger(DEFAULT_CHANGE_EVENT) diff --git a/frontend/common/stores/config-store.js b/frontend/common/stores/config-store.js index 38f79c4c319a..f0e7b4aa82da 100644 --- a/frontend/common/stores/config-store.js +++ b/frontend/common/stores/config-store.js @@ -22,8 +22,25 @@ const controller = { } store.model = flagsmith.getAllFlags() }, - onError() { - store.error = true + onError(e) { + if ( + Project.isFlagsmithOnFlagsmith || + (!Project.flagsmith && !Project.flagsmithClientAPI) + ) { + store.model = {} + // TODO: Migrate to TS and use enum + store.error = { + message: e?.message, + type: 'fof_init_error', + } + store.loaded(true) + return + } + + store.error = { + message: e, + type: 'unknown', + } store.goneABitWest() }, } @@ -54,8 +71,9 @@ flagsmith onChange: controller.loaded, realtime: Project.flagsmithRealtime, }) - .catch(() => { - controller.onError() + .catch((e) => { + console.error(e) + controller.onError(e) }) controller.store = store diff --git a/frontend/common/utils/utils.tsx b/frontend/common/utils/utils.tsx index f0323e1c2ed3..359e2436393b 100644 --- a/frontend/common/utils/utils.tsx +++ b/frontend/common/utils/utils.tsx @@ -207,7 +207,8 @@ const Utils = Object.assign({}, require('./base/_utils'), { * only add behaviour to Flagsmith-on-Flagsmith flags that have been explicitly created by customers. */ flagsmithFeatureExists(flag: string) { - return Object.prototype.hasOwnProperty.call(flagsmith.getAllFlags(), flag) + const allFlags = flagsmith?.getAllFlags() + return allFlags && Object.prototype.hasOwnProperty.call(allFlags, flag) }, getContentType(contentTypes: ContentType[], model: string, type: string) { return contentTypes.find((c: ContentType) => c[model] === type) || null @@ -403,6 +404,53 @@ const Utils = Object.assign({}, require('./base/_utils'), { } return true }, + getExistingWaitForTime: ( + waitFor: string | undefined, + ): { amountOfTime: number; timeUnit: (typeof TimeUnit)[keyof typeof TimeUnit] } | undefined => { + if (!waitFor) { + return + } + + const timeParts = waitFor.split(':') + + if (timeParts.length != 3) return + + const [hours, minutes, seconds] = timeParts + + const amountOfMinutes = Number(minutes) + const amountOfHours = Number(hours) + const amountOfSeconds = Number(seconds) + + if (amountOfHours + amountOfMinutes + amountOfSeconds === 0) { + return + } + + // Days + if ( + amountOfHours % 24 === 0 && + amountOfMinutes === 0 && + amountOfSeconds === 0 + ) { + return { + amountOfTime: amountOfHours / 24, + timeUnit: TimeUnit.DAY, + } + } + + // Hours + if (amountOfHours > 0 && amountOfMinutes === 0 && amountOfSeconds === 0) { + return { + amountOfTime: amountOfHours, + timeUnit: TimeUnit.HOUR, + } + } + + // Minutes + return { + amountOfTime: amountOfMinutes, + timeUnit: TimeUnit.MINUTE, + } + }, getPlansPermission: (feature: PaidFeature) => { const isOrgPermission = feature !== '2FA' const plans = isOrgPermission @@ -423,6 +471,7 @@ const Utils = Object.assign({}, require('./base/_utils'), { getProjectColour(index: number) { return Constants.projectColors[index % (Constants.projectColors.length - 1)] }, + getRequiredPlan: (feature: PaidFeature) => { let plan switch (feature) { @@ -538,58 +587,10 @@ const Utils = Object.assign({}, require('./base/_utils'), { return str }, - getViewIdentitiesPermission() { return 'VIEW_IDENTITIES' }, - getExistingWaitForTime: ( - waitFor: string | undefined, - ): { amountOfTime: number; timeUnit: (typeof TimeUnit)[keyof typeof TimeUnit] } | undefined => { - if (!waitFor) { - return - } - - const timeParts = waitFor.split(':') - - if (timeParts.length != 3) return - - const [hours, minutes, seconds] = timeParts - - const amountOfMinutes = Number(minutes) - const amountOfHours = Number(hours) - const amountOfSeconds = Number(seconds) - - if (amountOfHours + amountOfMinutes + amountOfSeconds === 0) { - return - } - // Days - if ( - amountOfHours % 24 === 0 && - amountOfMinutes === 0 && - amountOfSeconds === 0 - ) { - return { - amountOfTime: amountOfHours / 24, - timeUnit: TimeUnit.DAY, - } - } - - // Hours - if (amountOfHours > 0 && amountOfMinutes === 0 && amountOfSeconds === 0) { - return { - amountOfTime: amountOfHours, - timeUnit: TimeUnit.HOUR, - } - } - - // Minutes - return { - amountOfTime: amountOfMinutes, - timeUnit: TimeUnit.MINUTE, - } - }, - hasEntityPermission(key: string, entityPermissions: UserPermissions) { if (entityPermissions?.admin) return true return !!entityPermissions?.permissions?.find( diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 516f1e5ad341..110f0ddd5f8c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9944,7 +9944,8 @@ "node_modules/flagsmith": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/flagsmith/-/flagsmith-9.0.0.tgz", - "integrity": "sha512-hIipa+/ymTYtyEeDU181iEiZ6NbSXEpcYqJdRaY2A9zRhDh3sHNCqXvtWMxOuVOrBEt4HZZ/IsJ4eAsIV5me9A==" + "integrity": "sha512-hIipa+/ymTYtyEeDU181iEiZ6NbSXEpcYqJdRaY2A9zRhDh3sHNCqXvtWMxOuVOrBEt4HZZ/IsJ4eAsIV5me9A==", + "license": "BSD-3-Clause" }, "node_modules/flat": { "version": "5.0.2", diff --git a/frontend/web/components/App.js b/frontend/web/components/App.js index 89775baabaf7..8e6389b2a5bb 100644 --- a/frontend/web/components/App.js +++ b/frontend/web/components/App.js @@ -1,4 +1,4 @@ -import React, { Component, Fragment } from 'react' +import React, { Component } from 'react' import { matchPath, withRouter } from 'react-router-dom' import * as amplitude from '@amplitude/analytics-browser' import { plugin as engagementPlugin } from '@amplitude/engagement-browser' @@ -16,6 +16,10 @@ import { resolveAuthFlow } from '@datadog/ui-extensions-sdk' import ConfigProvider from 'common/providers/ConfigProvider' import AccountStore from 'common/stores/account-store' import OrganisationLimit from './OrganisationLimit' +import { + getStartupErrorText, + isFlagsmithOnFlagsmithError, +} from './base/errors/init.error' import OrganisationStore from 'common/stores/organisation-store' import ScrollToTop from './ScrollToTop' import AnnouncementPerPage from './AnnouncementPerPage' @@ -240,9 +244,32 @@ const App = class extends Component { ) { return } - if (Project.maintenance || this.props.error || !window.projectOverrides) { + + const maintenanceMode = + Utils.getFlagsmithHasFeature('maintenance_mode') || Project.maintenance + const isUnknownError = + this.props.error && !isFlagsmithOnFlagsmithError(this.props.error) + if (maintenanceMode || !window.projectOverrides || isUnknownError) { return } + + if (this.props.error && isFlagsmithOnFlagsmithError(this.props.error)) { + toast( + getStartupErrorText(this.props.error), + 'danger', + 2 * 60 * 1000, + { + buttonText: 'See documentation', + onClick: () => + window.open( + 'https://docs.flagsmith.com/deployment/#running-flagsmith-on-flagsmith', + '_blank', + ), + }, + { size: 'large' }, + ) + } + const activeProject = OrganisationStore.getProject(projectId) const projectNotLoaded = !activeProject && document.location.href.includes('project/') @@ -262,12 +289,15 @@ const App = class extends Component { ) } + if (AccountStore.forced2Factor()) { return } + if (document.location.pathname.includes('widget')) { return
{this.props.children}
} + return ( {user && ( <> diff --git a/frontend/web/components/ButterBar.tsx b/frontend/web/components/ButterBar.tsx index 7803de3cfc96..4a9bdbcd7388 100644 --- a/frontend/web/components/ButterBar.tsx +++ b/frontend/web/components/ButterBar.tsx @@ -13,9 +13,14 @@ import Constants from 'common/constants' interface ButterBarProps { billingStatus?: string projectId: string + fofError?: string } -const ButterBar: React.FC = ({ billingStatus, projectId }) => { +const ButterBar: React.FC = ({ + billingStatus, + fofError, + projectId, +}) => { const matches = document.location.href.match(/\/environment\/([^/]*)/) const environment = matches && matches[1] const timerRef = useRef() @@ -59,6 +64,35 @@ const ButterBar: React.FC = ({ billingStatus, projectId }) => { return processing }, [checkProcessing, featureImports]) + if (fofError) { + return ( +
+ +
Could not initialise Flagsmith-on-Flagsmith
+
+

The error was: "{fofError}"

+

+ Flag evaluation for is not affected, but some dashboard features might + be unavailable. +

+

+ Check with{' '} + + Flagsmith-on-Flagsmith documentation + {' '} + for more info. +

+
+ ) + } + if (processingImport) { return (
diff --git a/frontend/web/components/Maintenance.js b/frontend/web/components/Maintenance.tsx similarity index 51% rename from frontend/web/components/Maintenance.js rename to frontend/web/components/Maintenance.tsx index db7d8fe6a3e2..101d9e31ad31 100644 --- a/frontend/web/components/Maintenance.js +++ b/frontend/web/components/Maintenance.tsx @@ -1,34 +1,21 @@ import React from 'react' import ConfigProvider from 'common/providers/ConfigProvider' -const HomePage = class extends React.Component { - static displayName = 'HomePage' - - constructor(props, context) { - super(props, context) - this.state = {} - } - - render = () => ( +const MaintenancePage: React.FC = () => { + return (

Maintenance

We are currently undergoing some scheduled maintenance of the admin site, this will not affect your application's feature flags. - { - <> - {' '} - Check{' '} - - @getflagsmith - {' '} - for updates. - - } + <> + {' '} + Check{' '} + + @getflagsmith + {' '} + for updates. +

Sorry for the inconvenience, we will be back up and running shortly. @@ -38,4 +25,6 @@ const HomePage = class extends React.Component { ) } -module.exports = ConfigProvider(HomePage) +MaintenancePage.displayName = 'MaintenancePage' + +export default ConfigProvider(MaintenancePage) diff --git a/frontend/web/components/base/errors/init.error.ts b/frontend/web/components/base/errors/init.error.ts new file mode 100644 index 000000000000..6600651ebf89 --- /dev/null +++ b/frontend/web/components/base/errors/init.error.ts @@ -0,0 +1,37 @@ +export enum FlagsmithStartupErrors { + FOF_INIT_ERROR = 'fof_init_error', + UNKNOWN_ERROR = 'unknown_error', + MAINTENANCE_MODE = 'maintenance_mode', +} + +export interface SDKInitErrors { + type: FlagsmithStartupErrors + message: string +} + +const errorLabels: Record = { + [FlagsmithStartupErrors.FOF_INIT_ERROR]: + 'Please check your Flagsmith configuration', + [FlagsmithStartupErrors.MAINTENANCE_MODE]: 'Maintenance mode', + [FlagsmithStartupErrors.UNKNOWN_ERROR]: + 'Unexpected error. If it persists, please contact support', +} + +export const getStartupErrorText = (error: SDKInitErrors) => { + switch (error?.type) { + case FlagsmithStartupErrors.FOF_INIT_ERROR: + return `${error?.message}. ${errorLabels[error?.type]}` + case FlagsmithStartupErrors.MAINTENANCE_MODE: + return errorLabels[error?.type] + default: + return errorLabels[FlagsmithStartupErrors.UNKNOWN_ERROR] + } +} + +export const isMaintenanceError = (error: SDKInitErrors) => { + return error?.type === FlagsmithStartupErrors.MAINTENANCE_MODE +} + +export const isFlagsmithOnFlagsmithError = (error: SDKInitErrors) => { + return error?.type === FlagsmithStartupErrors.FOF_INIT_ERROR +} diff --git a/frontend/web/components/release-pipelines/StageFeatureDetail.tsx b/frontend/web/components/release-pipelines/StageFeatureDetail.tsx index 7d3ad70f68a0..689e4a987f1e 100644 --- a/frontend/web/components/release-pipelines/StageFeatureDetail.tsx +++ b/frontend/web/components/release-pipelines/StageFeatureDetail.tsx @@ -47,7 +47,6 @@ const StageFeatureDetail = ({ ) } - return ( <>

Features ({features.length})
diff --git a/frontend/web/project/toast.tsx b/frontend/web/project/toast.tsx index f2cd888f0f6c..81d631892d76 100644 --- a/frontend/web/project/toast.tsx +++ b/frontend/web/project/toast.tsx @@ -22,12 +22,14 @@ export interface MessageProps { isRemoving?: boolean theme?: ThemeType children?: React.ReactNode + extraStyles?: { size?: 'large' | undefined } } const Message: FC = ({ action, children, expiry = 5000, + extraStyles, isRemoving = false, remove, theme = 'success', @@ -40,6 +42,7 @@ const Message: FC = ({ const className = cn( { 'alert': true, + 'large': extraStyles?.size === 'large', 'removing-out': isRemoving, 'show': !isRemoving, 'toast-message': true, @@ -87,6 +90,7 @@ export interface Message { expiry?: number theme?: ThemeType isRemoving?: boolean + extraStyles?: { size?: 'large' | undefined } } const ToastMessages: FC<{}> = () => { @@ -97,6 +101,7 @@ const ToastMessages: FC<{}> = () => { theme?: ThemeType, expiry?: number, action?: { buttonText: string; onClick: () => void }, + extraStyles?: { size?: 'large' | undefined }, ) => { setMessages((prevMessages) => { // Ignore duplicate messages @@ -110,6 +115,7 @@ const ToastMessages: FC<{}> = () => { action, content, expiry: E2E ? 1000 : expiry, + extraStyles, id, theme, }, @@ -149,6 +155,7 @@ const ToastMessages: FC<{}> = () => { remove={() => remove(message.id)} expiry={message.expiry} theme={message.theme} + extraStyles={message.extraStyles} > {message.content} diff --git a/frontend/web/styles/components/_toast.scss b/frontend/web/styles/components/_toast.scss index 6cb0b5eaee23..a2ebb94f2d5e 100644 --- a/frontend/web/styles/components/_toast.scss +++ b/frontend/web/styles/components/_toast.scss @@ -66,6 +66,10 @@ animation: toast-out 0.3s ease-out forwards; } + &.large { + width: 520px !important; + } + @media screen and (min-width: 768px) { width: 320px; }