Skip to content

Commit

Permalink
chore: Plan based access UI (#4281)
Browse files Browse the repository at this point in the history
  • Loading branch information
kyle-ssg authored Aug 14, 2024
1 parent f9cc1b0 commit 6ba44f8
Show file tree
Hide file tree
Showing 35 changed files with 1,296 additions and 1,071 deletions.
16 changes: 15 additions & 1 deletion frontend/common/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { OAuthType } from './types/requests'
import { SegmentCondition } from './types/responses'
import Utils from './utils/utils'

import Project from './project'
const keywords = {
FEATURE_FUNCTION: 'myCoolFeature',
Expand Down Expand Up @@ -348,6 +350,12 @@ export default {
}
},
'VIEW_FEATURE': { 'category': 'Features', 'event': 'Feature viewed' },
VIEW_LOCKED_FEATURE: (feature: string) => {
return {
'category': 'Locked Feature',
'event': `View Locked Feature ${feature}`,
}
},
'VIEW_SEGMENT': { 'category': 'Segment', 'event': 'Segment viewed' },
'VIEW_USER_FEATURE': {
'category': 'User Features',
Expand Down Expand Up @@ -434,6 +442,13 @@ export default {
'TRAITS_ID': 150,
},
},
getUpgradeUrl: (feature?: string) => {
return Utils.isSaas()
? '/organisation-settings?tab=billing'
: `https://www.flagsmith.com/pricing${
feature ? `utm_source=${feature}` : ''
}`
},
githubType: {
githubIssue: 'GitHub Issue',
githubPR: 'Github PR',
Expand Down Expand Up @@ -549,5 +564,4 @@ export default {
'#DE3163',
],
untaggedTag: { color: '#dedede', label: 'Untagged' },
upgradeURL: '/organisation-settings?tab=billing',
}
153 changes: 87 additions & 66 deletions frontend/common/utils/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@ import Format from './format'

const semver = require('semver')

export type PaidFeature =
| 'FLAG_OWNERS'
| 'RBAC'
| 'AUDIT'
| 'FORCE_2FA'
| '4_EYES'
| 'STALE_FLAGS'
| 'VERSIONING'
| 'AUTO_SEATS'
| 'METADATA'
| 'REALTIME'
| 'SAML'
| 'SCHEDULE_FLAGS'
| 'CREATE_ADDITIONAL_PROJECT'
| '2FA'

// Define a type for plan categories
type Plan = 'start-up' | 'scale-up' | 'enterprise' | null

export const planNames = {
enterprise: 'Enterprise',
free: 'Free',
Expand Down Expand Up @@ -268,13 +287,28 @@ const Utils = Object.assign({}, require('./base/_utils'), {
getManageUserPermissionDescription() {
return 'Manage Identities'
},
getNextPlan: (skipFree?: boolean) => {
const currentPlan = Utils.getPlanName(AccountStore.getActiveOrgPlan())
switch (currentPlan) {
case planNames.free: {
return skipFree ? planNames.startup : planNames.scaleUp
}
case planNames.startup: {
return planNames.startup
}
default: {
return planNames.enterprise
}
}
},
getOrganisationHomePage(id?: string) {
const orgId = id || AccountStore.getOrganisation()?.id
if (!orgId) {
return `/organisations`
}
return `/organisation/${orgId}/projects`
},

getPermissionList(
isAdmin: boolean,
permissions: string[] | undefined | null,
Expand Down Expand Up @@ -305,8 +339,10 @@ const Utils = Object.assign({}, require('./base/_utils'), {
.map((item) => `${Format.enumeration.get(item)}`),
}
},

getPlanName: (plan: string) => {
if (plan && plan.includes('free')) {
return planNames.free
}
if (plan && plan.includes('scale-up')) {
return planNames.scaleUp
}
Expand All @@ -324,77 +360,27 @@ const Utils = Object.assign({}, require('./base/_utils'), {
}
return planNames.free
},
getPlanPermission: (plan: string, permission: string) => {
let valid = true
getPlanPermission: (plan: string, feature: PaidFeature) => {
const planName = Utils.getPlanName(plan)

if (!plan || planName === planNames.free) {
return false
}
const isScaleupOrGreater = planName !== planNames.startup
const isEnterprise = planName === planNames.enterprise
const isSaas = Utils.isSaas()
switch (permission) {
case 'FLAG_OWNERS': {
valid = isScaleupOrGreater
break
}
case 'CREATE_ADDITIONAL_PROJECT': {
valid = true // startup or greater
break
}
case '2FA': {
valid = true // startup or greater
break
}
case 'RBAC': {
valid = isScaleupOrGreater
break
}
case 'AUDIT': {
valid = isScaleupOrGreater
break
}
case 'AUTO_SEATS': {
valid = isScaleupOrGreater && !isEnterprise
break
}
case 'FORCE_2FA': {
valid = isScaleupOrGreater
break
}
case 'SCHEDULE_FLAGS': {
valid = true // startup or greater
break
}
case '4_EYES': {
valid = isScaleupOrGreater
break
}
case 'REALTIME': {
valid = isEnterprise && isSaas
break
}
case 'STALE_FLAGS': {
valid = isEnterprise
break
}
case 'SAML': {
valid = isEnterprise
break
}
case 'METADATA': {
valid = isEnterprise
break
}
default:
valid = true
break
if (feature === 'AUTO_SEATS') {
return isScaleupOrGreater && !isEnterprise
}

const requiredPlan = Utils.getRequiredPlan(feature)
if (requiredPlan === 'enterprise') {
return isEnterprise
} else if (requiredPlan === 'scale-up') {
return isScaleupOrGreater
}
return valid
return true
},
getPlansPermission: (permission: string) => {
const isOrgPermission = permission !== '2FA'
getPlansPermission: (feature: PaidFeature) => {
const isOrgPermission = feature !== '2FA'
const plans = isOrgPermission
? AccountStore.getActiveOrgPlan()
? [AccountStore.getActiveOrgPlan()]
Expand All @@ -405,16 +391,51 @@ const Utils = Object.assign({}, require('./base/_utils'), {
return false
}
const found = _.find(
plans.map((plan: string) => Utils.getPlanPermission(plan, permission)),
plans.map((plan: string) => Utils.getPlanPermission(plan, feature)),
(perm) => !!perm,
)
return !!found
},

getProjectColour(index: number) {
return Constants.projectColors[index % (Constants.projectColors.length - 1)]
},

getRequiredPlan: (feature: PaidFeature) => {
let plan
switch (feature) {
case 'FLAG_OWNERS':
case 'RBAC':
case 'AUDIT':
case 'FORCE_2FA':
case '4_EYES': {
plan = 'scale-up'
break
}
case 'STALE_FLAGS':
case 'REALTIME':
case 'METADATA':
case 'SAML': {
plan = 'enterprise'
break
}

case 'SCHEDULE_FLAGS':
case 'CREATE_ADDITIONAL_PROJECT':
case '2FA': {
plan = 'start-up' // startup or greater
break
}
default: {
plan = null
break
}
}
if (plan && !Utils.isSaas()) {
plan = 'enterprise'
}
return plan as Plan
},

getSDKEndpoint(_project: ProjectType) {
const project = _project || ProjectStore.model

Expand Down
9 changes: 1 addition & 8 deletions frontend/web/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -578,15 +578,8 @@ const App = class extends Component {
id={projectId}
>
{({ permission }) =>
permission &&
Utils.getPlansPermission('RBAC') && (
permission && (
<NavSubLink
tooltip={
!Utils.getPlansPermission('RBAC')
? 'This feature is available with our scaleup plan'
: ''
}
disabled={!Utils.getPlansPermission('RBAC')}
icon={<AuditLogIcon />}
id='audit-log-link'
to={`/project/${projectId}/audit-log`}
Expand Down
25 changes: 12 additions & 13 deletions frontend/web/components/AuditLog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Tag from './tags/Tag'
import PanelSearch from './PanelSearch'
import JSONReference from './JSONReference'
import moment from 'moment'
import PlanBasedBanner from './PlanBasedAccess'

type AuditLogType = {
environmentId: string
Expand Down Expand Up @@ -159,18 +160,6 @@ const AuditLog: FC<AuditLogType> = (props) => {

const { env: envFilter } = Utils.fromParam()

const hasRbacPermission = Utils.getPlansPermission('AUDIT')
if (!hasRbacPermission) {
return (
<div>
<div className='text-center'>
To access this feature please upgrade your account to scaleup or
higher.
</div>
</div>
)
}

return (
<PanelSearch
id='messages-list'
Expand Down Expand Up @@ -230,4 +219,14 @@ const AuditLog: FC<AuditLogType> = (props) => {
)
}

export default withRouter(AuditLog as any)
type AuditLogWrapperType = AuditLogType

const AuditLogWrapper: FC<AuditLogWrapperType> = (props) => {
return (
<PlanBasedBanner feature={'AUDIT'} theme={'page'}>
<AuditLog {...props} />
</PlanBasedBanner>
)
}

export default withRouter(AuditLogWrapper as any)
2 changes: 1 addition & 1 deletion frontend/web/components/ButterBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ const ButterBar: React.FC<ButterBarProps> = ({ billingStatus, projectId }) => {
{Utils.getFlagsmithHasFeature('read_only_mode') && (
<div className='butter-bar'>
Your organisation is over its usage limit, please{' '}
<Link to={Constants.upgradeURL}>upgrade your plan</Link>.
<Link to={Constants.getUpgradeUrl()}>upgrade your plan</Link>.
</div>
)}
{Utils.getFlagsmithHasFeature('show_dunning_banner') &&
Expand Down
Loading

0 comments on commit 6ba44f8

Please sign in to comment.