From 000be2b0c071d6ef839da5d0febcc85b58e47ab7 Mon Sep 17 00:00:00 2001 From: Novak Zaballa <41410593+novakzaballa@users.noreply.github.com> Date: Mon, 18 Sep 2023 11:28:59 -0400 Subject: [PATCH] feat: display warning and prevent creation on limit (#2526) Co-authored-by: Matthew Elwell Co-authored-by: kyle-ssg --- .prettierignore | 3 +- frontend/common/dispatcher/app-actions.js | 2 +- .../common/providers/FeatureListProvider.js | 6 +- frontend/common/services/useEnvironment.ts | 21 ++++ .../services/useSubscriptionMetadata.ts | 49 ++++++++++ frontend/common/stores/project-store.js | 31 ++++++ frontend/common/types/requests.ts | 3 + frontend/common/types/responses.ts | 8 ++ frontend/common/utils/utils.tsx | 35 +++++-- frontend/web/components/App.js | 10 +- frontend/web/components/ErrorMessage.js | 23 ++++- frontend/web/components/FlagSelect.js | 1 + frontend/web/components/Icon.tsx | 20 ++++ frontend/web/components/InfoMessage.js | 4 +- frontend/web/components/OrganisationLimit.tsx | 53 ++++++++++ frontend/web/components/SamlForm.js | 2 +- .../web/components/SegmentOverrideLimit.tsx | 31 ++++++ frontend/web/components/SegmentOverrides.js | 30 +++++- frontend/web/components/SegmentSelect.tsx | 2 + frontend/web/components/WarningMessage.tsx | 41 ++++++++ .../modals/AssociatedSegmentOverrides.js | 36 ++++++- frontend/web/components/modals/CreateFlag.js | 20 +++- .../web/components/modals/CreateSegment.tsx | 15 ++- frontend/web/components/modals/Payment.js | 11 +++ frontend/web/components/pages/FeaturesPage.js | 28 +++++- .../web/components/pages/SegmentsPage.tsx | 13 ++- frontend/web/components/svg/UpgradeIcon.js | 4 +- frontend/web/styles/_variables.scss | 97 ++++++++----------- frontend/web/styles/components/_toast.scss | 20 ++-- frontend/web/styles/new/_variables-new.scss | 4 +- frontend/web/styles/project/_alert.scss | 5 + 31 files changed, 528 insertions(+), 100 deletions(-) create mode 100644 frontend/common/services/useSubscriptionMetadata.ts create mode 100644 frontend/web/components/OrganisationLimit.tsx create mode 100644 frontend/web/components/SegmentOverrideLimit.tsx create mode 100644 frontend/web/components/WarningMessage.tsx diff --git a/.prettierignore b/.prettierignore index faf3a6717333..533547b5f889 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,4 +3,5 @@ *.json *.handlebars *.css -.bablerc \ No newline at end of file +.bablerc +**/CHANGELOG.md \ No newline at end of file diff --git a/frontend/common/dispatcher/app-actions.js b/frontend/common/dispatcher/app-actions.js index 5cf6810a29a0..76363b008c93 100644 --- a/frontend/common/dispatcher/app-actions.js +++ b/frontend/common/dispatcher/app-actions.js @@ -59,13 +59,13 @@ const AppActions = Object.assign({}, require('./base/_app-actions'), { name, }) }, - createProject(name) { Dispatcher.handleViewAction({ actionType: Actions.CREATE_PROJECT, name, }) }, + deleteChangeRequest(id, cb) { Dispatcher.handleViewAction({ actionType: Actions.DELETE_CHANGE_REQUEST, diff --git a/frontend/common/providers/FeatureListProvider.js b/frontend/common/providers/FeatureListProvider.js index ee41ca4994a1..b9f634216b17 100644 --- a/frontend/common/providers/FeatureListProvider.js +++ b/frontend/common/providers/FeatureListProvider.js @@ -1,5 +1,6 @@ import React from 'react' import FeatureListStore from 'common/stores/feature-list-store' +import ProjectStore from 'common/stores/project-store' const FeatureListProvider = class extends React.Component { static displayName = 'FeatureListProvider' @@ -11,8 +12,9 @@ const FeatureListProvider = class extends React.Component { isLoading: FeatureListStore.isLoading, isSaving: FeatureListStore.isSaving, lastSaved: FeatureListStore.getLastSaved(), + maxFeaturesAllowed: ProjectStore.getMaxFeaturesAllowed(), projectFlags: FeatureListStore.getProjectFlags(), - usageData: FeatureListStore.getFeatureUsage(), + totalFeatures: ProjectStore.getTotalFeatures(), } ES6Component(this) this.listenTo(FeatureListStore, 'change', () => { @@ -22,7 +24,9 @@ const FeatureListProvider = class extends React.Component { isLoading: FeatureListStore.isLoading, isSaving: FeatureListStore.isSaving, lastSaved: FeatureListStore.getLastSaved(), + maxFeaturesAllowed: ProjectStore.getMaxFeaturesAllowed(), projectFlags: FeatureListStore.getProjectFlags(), + totalFeatures: ProjectStore.getTotalFeatures(), usageData: FeatureListStore.getFeatureUsage(), }) }) diff --git a/frontend/common/services/useEnvironment.ts b/frontend/common/services/useEnvironment.ts index 33b6d1e900db..6330e72cc21f 100644 --- a/frontend/common/services/useEnvironment.ts +++ b/frontend/common/services/useEnvironment.ts @@ -6,6 +6,12 @@ export const environmentService = service .enhanceEndpoints({ addTagTypes: ['Environment'] }) .injectEndpoints({ endpoints: (builder) => ({ + getEnvironment: builder.query({ + providesTags: (res) => [{ id: res?.id, type: 'Environment' }], + query: (query: Req['getEnvironment']) => ({ + url: `environments/${query.id}/`, + }), + }), getEnvironments: builder.query< Res['environments'], Req['getEnvironments'] @@ -33,9 +39,24 @@ export async function getEnvironments( store.dispatch(environmentService.util.getRunningQueriesThunk()), ) } +export async function getEnvironment( + store: any, + data: Req['getEnvironment'], + options?: Parameters< + typeof environmentService.endpoints.getEnvironment.initiate + >[1], +) { + store.dispatch( + environmentService.endpoints.getEnvironment.initiate(data, options), + ) + return Promise.all( + store.dispatch(environmentService.util.getRunningQueriesThunk()), + ) +} // END OF FUNCTION_EXPORTS export const { + useGetEnvironmentQuery, useGetEnvironmentsQuery, // END OF EXPORTS } = environmentService diff --git a/frontend/common/services/useSubscriptionMetadata.ts b/frontend/common/services/useSubscriptionMetadata.ts new file mode 100644 index 000000000000..86b12e38af5a --- /dev/null +++ b/frontend/common/services/useSubscriptionMetadata.ts @@ -0,0 +1,49 @@ +import { Res } from 'common/types/responses' +import { Req } from 'common/types/requests' +import { service } from 'common/service' + +export const getSubscriptionMetadataService = service + .enhanceEndpoints({ addTagTypes: ['GetSubscriptionMetadata'] }) + .injectEndpoints({ + endpoints: (builder) => ({ + getSubscriptionMetadata: builder.query< + Res['getSubscriptionMetadata'], + Req['getSubscriptionMetadata'] + >({ + providesTags: (res) => [ + { id: res?.id, type: 'GetSubscriptionMetadata' }, + ], + query: (query: Req['getSubscriptionMetadata']) => ({ + url: `organisations/${query.id}/get-subscription-metadata/`, + }), + }), + // END OF ENDPOINTS + }), + }) + +export async function getSubscriptionMetadata( + store: any, + data: Req['getSubscriptionMetadata'], + options?: Parameters< + typeof getSubscriptionMetadataService.endpoints.getSubscriptionMetadata.initiate + >[1], +) { + return store.dispatch( + getSubscriptionMetadataService.endpoints.getSubscriptionMetadata.initiate( + data, + options, + ), + ) +} +// END OF FUNCTION_EXPORTS + +export const { + useGetSubscriptionMetadataQuery, + // END OF EXPORTS +} = getSubscriptionMetadataService + +/* Usage examples: +const { data, isLoading } = useGetSubscriptionMetadataQuery({ id: 2 }, {}) //get hook +const [getSubscriptionMetadata, { isLoading, data, isSuccess }] = useGetSubscriptionMetadataMutation() //create hook +getSubscriptionMetadataService.endpoints.getSubscriptionMetadata.select({id: 2})(store.getState()) //access data from any function +*/ diff --git a/frontend/common/stores/project-store.js b/frontend/common/stores/project-store.js index 201445190acd..18b9ef138064 100644 --- a/frontend/common/stores/project-store.js +++ b/frontend/common/stores/project-store.js @@ -1,6 +1,7 @@ import { getIsWidget } from 'components/pages/WidgetPage' import Constants from 'common/constants' +import Utils from 'common/utils/utils' const Dispatcher = require('../dispatcher/dispatcher') const BaseStore = require('./base/_store') @@ -92,6 +93,12 @@ const controller = { data.get(`${Project.api}environments/?project=${id}`).catch(() => []), ]) .then(([project, environments]) => { + project.max_segments_allowed = project.max_segments_allowed + project.max_features_allowed = project.max_features_allowed + project.max_segment_overrides_allowed = + project.max_segment_overrides_allowed + project.total_features = project.total_features || 0 + project.total_segments = project.total_segments || 0 store.model = Object.assign(project, { environments: _.sortBy(environments.results, 'name'), }) @@ -118,6 +125,12 @@ const controller = { data.get(`${Project.api}environments/?project=${id}`).catch(() => []), ]) .then(([project, environments]) => { + project.max_segments_allowed = project.max_segments_allowed + project.max_features_allowed = project.max_features_allowed + project.max_segment_overrides_allowed = + project.max_segment_overrides_allowed + project.total_features = project.total_features || 0 + project.total_segments = project.total_segments || 0 store.model = Object.assign(project, { environments: _.sortBy(environments.results, 'name'), }) @@ -162,6 +175,24 @@ const store = Object.assign({}, BaseStore, { }) }, getEnvs: () => store.model && store.model.environments, + getMaxFeaturesAllowed: () => { + return store.model && store.model.max_features_allowed + }, + getMaxSegmentOverridesAllowed: () => { + return store.model && store.model.max_segment_overrides_allowed + }, + getMaxSegmentsAllowed: () => { + return store.model && store.model.max_segments_allowed + }, + getTotalFeatures: () => { + return store.model && store.model.total_features + }, + getTotalSegmentOverrides: () => { + return store.model && store.model.environment.total_segment_overrides + }, + getTotalSegments: () => { + return store.model && store.model.total_segments + }, id: 'project', model: null, }) diff --git a/frontend/common/types/requests.ts b/frontend/common/types/requests.ts index c7a6d2d98bb0..af3ad316bc30 100644 --- a/frontend/common/types/requests.ts +++ b/frontend/common/types/requests.ts @@ -105,5 +105,8 @@ export type Req = { user: string } getProjectFlags: { project: string } + getGetSubscriptionMetadata: { id: string } + getEnvironment: { id: string } + getSubscriptionMetadata: { id: string } // END OF TYPES } diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index 8ad3aacf5e70..843f9a2c1c83 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -50,6 +50,7 @@ export type Environment = { minimum_change_request_approvals?: number allow_client_traits: boolean hide_sensitive_data: boolean + total_segment_overrides?: number } export type Project = { id: number @@ -62,6 +63,11 @@ export type Project = { use_edge_identities: boolean prevent_flag_defaults: boolean enable_realtime_updates: boolean + max_segments_allowed?: number | null + max_features_allowed?: number | null + max_segment_overrides_allowed?: number | null + total_features?: number + total_segments?: number environments: Environment[] } @@ -336,5 +342,7 @@ export type Res = { projectFlags: PagedResponse identityFeatureStates: IdentityFeatureState[] + getSubscriptionMetadata: { id: string } + environment: Environment // END OF TYPES } diff --git a/frontend/common/utils/utils.tsx b/frontend/common/utils/utils.tsx index 9b1c1cfea8d1..e863a5e3b461 100644 --- a/frontend/common/utils/utils.tsx +++ b/frontend/common/utils/utils.tsx @@ -14,6 +14,8 @@ import { import flagsmith from 'flagsmith' import { ReactNode } from 'react' import _ from 'lodash' +import ErrorMessage from 'components/ErrorMessage' +import WarningMessage from 'components/WarningMessage' import Constants from 'common/constants' const semver = require('semver') @@ -54,25 +56,40 @@ const Utils = Object.assign({}, require('./base/_utils'), { return 100 - total }, - calculaterRemainingCallsPercentage(value, total) { - const minRemainingPercentage = 30 + calculateRemainingLimitsPercentage( + total: number | undefined, + max: number | undefined, + threshold = 90, + ) { if (total === 0) { return 0 } - - const percentage = (value / total) * 100 - const remainingPercentage = 100 - percentage - - if (remainingPercentage <= minRemainingPercentage) { - return true + const percentage = (total / max) * 100 + if (percentage >= threshold) { + return { + percentage: Math.floor(percentage), + } } - return false + return 0 }, changeRequestsEnabled(value: number | null | undefined) { return typeof value === 'number' }, + displayLimitAlert(type: string, percentage: number | undefined) { + const envOrProject = + type === 'segment overrides' ? 'environment' : 'project' + return percentage >= 100 ? ( + + ) : percentage ? ( + + ) : null + }, escapeHtml(html: string) { const text = document.createTextNode(html) const p = document.createElement('p') diff --git a/frontend/web/components/App.js b/frontend/web/components/App.js index c2ca0c3a374a..ca1c2f9239b1 100644 --- a/frontend/web/components/App.js +++ b/frontend/web/components/App.js @@ -20,9 +20,10 @@ import ConfigProvider from 'common/providers/ConfigProvider' import Permission from 'common/providers/Permission' import { getOrganisationUsage } from 'common/services/useOrganisationUsage' import Button from './base/forms/Button' -import Icon from 'components/Icon' +import Icon from './Icon' import AccountStore from 'common/stores/account-store' import InfoMessage from './InfoMessage' +import OrganisationLimit from './OrganisationLimit' const App = class extends Component { static propTypes = { @@ -40,7 +41,6 @@ const App = class extends Component { lastProjectId: '', pin: '', showAnnouncement: true, - totalApiCalls: 0, } constructor(props, context) { @@ -75,7 +75,6 @@ const App = class extends Component { }).then((res) => { this.setState({ activeOrganisation: AccountStore.getOrganisation().id, - totalApiCalls: res?.data?.totals.total, }) }) } @@ -484,6 +483,11 @@ const App = class extends Component { ) : ( + {user && ( + + )} {user && showBanner && Utils.getFlagsmithHasFeature('announcement') && diff --git a/frontend/web/components/ErrorMessage.js b/frontend/web/components/ErrorMessage.js index 0456e9e5ab51..d91dc2e94c9e 100644 --- a/frontend/web/components/ErrorMessage.js +++ b/frontend/web/components/ErrorMessage.js @@ -1,13 +1,20 @@ // import propTypes from 'prop-types'; import React, { PureComponent } from 'react' import Icon from './Icon' +import PaymentModal from './modals/Payment' export default class ErrorMessage extends PureComponent { static displayName = 'ErrorMessage' render() { + const errorMessageClassName = `alert alert-danger ${ + this.props.errorMessageClass || 'flex-1 align-items-center' + }` return this.props.error ? ( -
+
@@ -16,6 +23,20 @@ export default class ErrorMessage extends PureComponent { .map((v) => `${v}: ${this.props.error[v]}`) .join('\n') : this.props.error} + {this.props.enabledButton && ( + + )}
) : null } diff --git a/frontend/web/components/FlagSelect.js b/frontend/web/components/FlagSelect.js index 1f2ccecad229..fd3e449b57ac 100644 --- a/frontend/web/components/FlagSelect.js +++ b/frontend/web/components/FlagSelect.js @@ -64,6 +64,7 @@ class FlagSelect extends Component { ? options.find((v) => v.value === this.props.value) : null } + isDisabled={this.props.disabled} onInputChange={this.search} placeholder={this.props.placeholder} onChange={(v) => this.props.onChange(v.value, v.flag)} diff --git a/frontend/web/components/Icon.tsx b/frontend/web/components/Icon.tsx index 3202513310b4..668c240a0a04 100644 --- a/frontend/web/components/Icon.tsx +++ b/frontend/web/components/Icon.tsx @@ -29,6 +29,7 @@ export type IconName = | 'person' | 'edit-outlined' | 'refresh' + | 'warning' | 'bell' | 'layout' | 'height' @@ -631,6 +632,25 @@ const Icon: FC = ({ fill, fill2, height, name, width, ...rest }) => { ) } + case 'warning': { + return ( + + + + ) + } case 'bell': { return ( - + )}
diff --git a/frontend/web/components/OrganisationLimit.tsx b/frontend/web/components/OrganisationLimit.tsx new file mode 100644 index 000000000000..24e58d86a16e --- /dev/null +++ b/frontend/web/components/OrganisationLimit.tsx @@ -0,0 +1,53 @@ +import { FC } from 'react' +import WarningMessage from './WarningMessage' +import ErrorMessage from './ErrorMessage' +import Utils from 'common/utils/utils' +import { useGetSubscriptionMetadataQuery } from 'common/services/useSubscriptionMetadata' +import Format from 'common/utils/format' +import { useGetOrganisationUsageQuery } from 'common/services/useOrganisationUsage' + +type OrganisationLimitType = { + id: string +} + +const OrganisationLimit: FC = ({ id }) => { + const { data: totalApiCalls } = useGetOrganisationUsageQuery({ + organisationId: id, + }) + const { data: maxApiCalls } = useGetSubscriptionMetadataQuery({ id }) + const maxApiCallsPercentage = Utils.calculateRemainingLimitsPercentage( + totalApiCalls?.totals.total, + maxApiCalls?.max_api_calls, + 70, + ).percentage + + const alertMaxApiCallsText = `You have used ${Format.shortenNumber( + totalApiCalls?.totals.total, + )}/${Format.shortenNumber( + maxApiCalls?.max_api_calls, + )} of your allowed requests.` + + return ( + + {Utils.getFlagsmithHasFeature('payments_enabled') && + Utils.getFlagsmithHasFeature('max_api_calls_alert') && + (maxApiCallsPercentage < 100 ? ( + + ) : ( + maxApiCallsPercentage >= 100 && ( + + ) + ))} + + ) +} + +export default OrganisationLimit diff --git a/frontend/web/components/SamlForm.js b/frontend/web/components/SamlForm.js index b5dbd1f02463..b4eddef2d304 100644 --- a/frontend/web/components/SamlForm.js +++ b/frontend/web/components/SamlForm.js @@ -3,7 +3,7 @@ import data from 'common/data/base/_data' import ErrorMessage from './ErrorMessage' import ConfigProvider from 'common/providers/ConfigProvider' import Icon from './Icon' -import ModalHR from 'components/modals/ModalHR' +import ModalHR from './modals/ModalHR' const SamlForm = class extends React.Component { static displayName = 'SamlForm' diff --git a/frontend/web/components/SegmentOverrideLimit.tsx b/frontend/web/components/SegmentOverrideLimit.tsx new file mode 100644 index 000000000000..036449756ff6 --- /dev/null +++ b/frontend/web/components/SegmentOverrideLimit.tsx @@ -0,0 +1,31 @@ +import { FC } from 'react' +import Utils from 'common/utils/utils' +import { useGetEnvironmentQuery } from 'common/services/useEnvironment' + +type SegmentOverrideLimitType = { + id: string + maxSegmentOverride: number +} + +const SegmentOverrideLimit: FC = ({ + id, + maxSegmentOverridesAllowed, +}) => { + const { data } = useGetEnvironmentQuery({ id }) + + const segmentOverrideLimitAlert = Utils.calculateRemainingLimitsPercentage( + data?.total_segment_overrides, + maxSegmentOverridesAllowed, + ) + + return ( + + {Utils.displayLimitAlert( + 'segment overrides', + segmentOverrideLimitAlert.percentage, + )} + + ) +} + +export default SegmentOverrideLimit diff --git a/frontend/web/components/SegmentOverrides.js b/frontend/web/components/SegmentOverrides.js index c9e7f270596c..0bf48bad439a 100644 --- a/frontend/web/components/SegmentOverrides.js +++ b/frontend/web/components/SegmentOverrides.js @@ -13,6 +13,9 @@ import InfoMessage from './InfoMessage' import Permission from 'common/providers/Permission' import Constants from 'common/constants' import Icon from './Icon' +import SegmentOverrideLimit from 'components/SegmentOverrideLimit' +import { getStore } from 'common/store' +import { getEnvironment } from 'common/services/useEnvironment' const arrayMoveMutate = (array, from, to) => { array.splice(to < 0 ? array.length + to : to, 0, array.splice(from, 1)[0]) @@ -424,7 +427,16 @@ class TheComponent extends Component { constructor(props) { super(props) - this.state = { segmentEditId: undefined } + this.state = { segmentEditId: undefined, totalSegmentOverrides: 0 } + } + componentDidMount() { + getEnvironment(getStore(), { + id: this.props.environmentId, + }).then((res) => { + this.setState({ + totalSegmentOverrides: res[0].data.total_segment_overrides, + }) + }) } addItem = () => { @@ -539,6 +551,14 @@ class TheComponent extends Component { const visibleValues = value && value.filter((v) => !v.toRemove) + const segmentOverrideLimitAlert = Utils.calculateRemainingLimitsPercentage( + this.state.totalSegmentOverrides, + ProjectStore.getMaxSegmentOverridesAllowed(), + ) + + const isLimitReached = + segmentOverrideLimitAlert.percentage && + segmentOverrideLimitAlert.percentage >= 100 return (
@@ -546,8 +566,9 @@ class TheComponent extends Component { !this.props.disableCreate && !this.props.showCreateSegment && !this.props.readOnly && ( - + Create Feature-Specific Segment @@ -647,6 +669,10 @@ class TheComponent extends Component { . +
)} diff --git a/frontend/web/components/SegmentSelect.tsx b/frontend/web/components/SegmentSelect.tsx index db10b640ee78..4139ed717d75 100644 --- a/frontend/web/components/SegmentSelect.tsx +++ b/frontend/web/components/SegmentSelect.tsx @@ -9,6 +9,7 @@ import Utils from 'common/utils/utils' import Button from './base/forms/Button' type SegmentSelectType = { + disabled: boolean projectId: string 'data-test'?: string placeholder?: string @@ -41,6 +42,7 @@ const SegmentSelect: FC = ({ data-test={rest['data-test']} placeholder={rest.placeholder} value={rest.value} + isDisabled={rest.disabled} onChange={rest.onChange} onInputChange={(e: any) => { searchItems(Utils.safeParseEventValue(e)) diff --git a/frontend/web/components/WarningMessage.tsx b/frontend/web/components/WarningMessage.tsx new file mode 100644 index 000000000000..a0793175717b --- /dev/null +++ b/frontend/web/components/WarningMessage.tsx @@ -0,0 +1,41 @@ +import React, { FC } from 'react' +import Icon from './Icon' +import PaymentModal from './modals/Payment' + +type WarningMessageType = { + warningMessage: string +} + +const WarningMessage: FC = (props) => { + const { enabledButton, warningMessage, warningMessageClass } = props + const warningMessageClassName = `alert alert-warning ${ + warningMessageClass || 'flex-1 align-items-center' + }` + return ( +
+ + + + {warningMessage} + {enabledButton && ( + + )} +
+ ) +} + +export default WarningMessage diff --git a/frontend/web/components/modals/AssociatedSegmentOverrides.js b/frontend/web/components/modals/AssociatedSegmentOverrides.js index a793d81995a3..297017bf271f 100644 --- a/frontend/web/components/modals/AssociatedSegmentOverrides.js +++ b/frontend/web/components/modals/AssociatedSegmentOverrides.js @@ -9,6 +9,9 @@ import SegmentOverrides from 'components/SegmentOverrides' import FlagSelect from 'components/FlagSelect' import InfoMessage from 'components/InfoMessage' import EnvironmentSelect from 'components/EnvironmentSelect' +import SegmentOverrideLimit from 'components/SegmentOverrideLimit' +import { getStore } from 'common/store' +import { getEnvironment } from 'common/services/useEnvironment' class TheComponent extends Component { state = { @@ -130,6 +133,10 @@ class TheComponent extends Component { . +
v.name === environmentId) + + if (!env) { + return + } + + const id = env.api_key + + getEnvironment(getStore(), { id }).then((res) => { + this.setState({ + totalSegmentOverrides: res[1].data.total_segment_overrides, + }) + }) + } componentDidMount() { ES6Component(this) + this.fetchTotalSegmentOverrides() } + componentDidUpdate(prevProps) { + if (prevProps.environmentId !== this.props.environmentId) { + this.fetchTotalSegmentOverrides() + } + } render() { const { environmentId, id, ignoreFlags, projectId } = this.props const addValue = (featureId, feature) => { @@ -409,6 +439,9 @@ class SegmentOverridesInnerAdd extends Component { // const newValue = ; // updateSegments(segmentOverrides.concat([newValue])) } + const segmentOverrideLimitAlert = + this.state.totalSegmentOverrides >= + ProjectStore.getMaxSegmentOverridesAllowed() return ( @@ -416,6 +449,7 @@ class SegmentOverridesInnerAdd extends Component { return (
+ {featureLimitAlert.percentage && + Utils.displayLimitAlert( + 'features', + featureLimitAlert.percentage, + )} @@ -1484,6 +1495,11 @@ const CreateFlag = class extends Component { !isEdit ? 'create-feature-tab px-3' : '', )} > + {featureLimitAlert.percentage && + Utils.displayLimitAlert( + 'features', + featureLimitAlert.percentage, + )} {Value( error, projectAdmin, @@ -1520,7 +1536,8 @@ const CreateFlag = class extends Component { isSaving || !name || invalid || - !regexValid + !regexValid || + featureLimitAlert.percentage >= 100 } > {isSaving ? 'Creating' : 'Create Feature'} @@ -1529,7 +1546,6 @@ const CreateFlag = class extends Component { )}
)} - {identity && (
{identity ? ( diff --git a/frontend/web/components/modals/CreateSegment.tsx b/frontend/web/components/modals/CreateSegment.tsx index 49c11d5b8300..48e63b6a4b9f 100644 --- a/frontend/web/components/modals/CreateSegment.tsx +++ b/frontend/web/components/modals/CreateSegment.tsx @@ -35,6 +35,7 @@ import ConfigProvider from 'common/providers/ConfigProvider' import JSONReference from 'components/JSONReference' import { cloneDeep } from 'lodash' import ErrorMessage from 'components/ErrorMessage' +import ProjectStore from 'common/stores/project-store' import Icon from 'components/Icon' type PageType = { @@ -134,6 +135,15 @@ const CreateSegment: FC = ({ const [tab, setTab] = useState(0) const isError = createError || updateError + const isLimitReached = + ProjectStore.getTotalSegments() >= ProjectStore.getMaxSegmentsAllowed() + + const THRESHOLD = 90 + const segmentsLimitAlert = Utils.calculateRemainingLimitsPercentage( + ProjectStore.getTotalSegments(), + ProjectStore.getMaxSegmentsAllowed(), + THRESHOLD, + ) const addRule = (type = 'ANY') => { const newRules = cloneDeep(rules) @@ -336,6 +346,8 @@ const CreateSegment: FC = ({ . + {segmentsLimitAlert.percentage && + Utils.displayLimitAlert('segments', segmentsLimitAlert.percentage)}
)} @@ -365,7 +377,6 @@ const CreateSegment: FC = ({
)} - {!condensed && ( = ({ ) : (
diff --git a/frontend/web/components/pages/FeaturesPage.js b/frontend/web/components/pages/FeaturesPage.js index e64564ed7de3..51bef8606e23 100644 --- a/frontend/web/components/pages/FeaturesPage.js +++ b/frontend/web/components/pages/FeaturesPage.js @@ -111,7 +111,8 @@ const FeaturesPage = class extends Component { if (!error?.name && !error?.initial_value) { // Could not determine field level error, show generic toast. toast( - 'We could not create this feature, please check the name is not in use.', + error.project || + 'We could not create this feature, please check the name is not in use.', 'danger', ) } @@ -161,8 +162,20 @@ const FeaturesPage = class extends Component { className='app-container container' > - {({ environmentFlags, projectFlags }, { removeFlag, toggleFlag }) => { + {( + { + environmentFlags, + maxFeaturesAllowed, + projectFlags, + totalFeatures, + }, + { removeFlag, toggleFlag }, + ) => { const isLoading = !FeatureListStore.hasLoaded + const featureLimitAlert = Utils.calculateRemainingLimitsPercentage( + totalFeatures, + maxFeaturesAllowed, + ) return (
{isLoading && (!projectFlags || !projectFlags.length) && ( @@ -178,6 +191,11 @@ const FeaturesPage = class extends Component { !!this.state.tags.length) && !isLoading) ? (
+ {featureLimitAlert.percentage && + Utils.displayLimitAlert( + 'features', + featureLimitAlert.percentage, + )} (