diff --git a/frontend/common/services/useFeatureVersion.ts b/frontend/common/services/useFeatureVersion.ts index 6f0f87de2a34..9e81a84bf60b 100644 --- a/frontend/common/services/useFeatureVersion.ts +++ b/frontend/common/services/useFeatureVersion.ts @@ -19,6 +19,7 @@ import { } from 'components/diff/diff-utils' import { getSegments } from './useSegment' import { getFeatureStates } from './useFeatureState' +import moment from 'moment' const transformFeatureStates = (featureStates: TypedFeatureState[]) => featureStates?.map((v) => ({ @@ -322,6 +323,17 @@ export const { // END OF EXPORTS } = featureVersionService +export function isVersionOverLimit( + versionLimitDays: number | null | undefined, + date: string | undefined, +) { + if (!versionLimitDays) { + return false + } + const days = moment().diff(moment(date), 'days') + 1 + return !!versionLimitDays && days > versionLimitDays +} + /* Usage examples: const { data, isLoading } = useGetFeatureVersionQuery({ id: 2 }, {}) //get hook const [createFeatureVersion, { isLoading, data, isSuccess }] = useCreateFeatureVersionMutation() //create hook diff --git a/frontend/common/services/useSubscriptionMetadata.ts b/frontend/common/services/useSubscriptionMetadata.ts index 86b12e38af5a..0ae46812779e 100644 --- a/frontend/common/services/useSubscriptionMetadata.ts +++ b/frontend/common/services/useSubscriptionMetadata.ts @@ -7,7 +7,7 @@ export const getSubscriptionMetadataService = service .injectEndpoints({ endpoints: (builder) => ({ getSubscriptionMetadata: builder.query< - Res['getSubscriptionMetadata'], + Res['subscriptionMetadata'], Req['getSubscriptionMetadata'] >({ providesTags: (res) => [ diff --git a/frontend/common/stores/organisation-store.js b/frontend/common/stores/organisation-store.js index f6ee6ebca7d6..a4796322e4c4 100644 --- a/frontend/common/stores/organisation-store.js +++ b/frontend/common/stores/organisation-store.js @@ -2,6 +2,7 @@ import Constants from 'common/constants' import { projectService } from 'common/services/useProject' import { getStore } from 'common/store' import sortBy from 'lodash/sortBy' +import { getSubscriptionMetadata } from 'common/services/useSubscriptionMetadata' const Dispatcher = require('../dispatcher/dispatcher') const BaseStore = require('./base/_store') @@ -139,11 +140,7 @@ const controller = { AccountStore.getOrganisationRole(id) === 'ADMIN' ? [ data.get(`${Project.api}organisations/${id}/invites/`), - data - .get( - `${Project.api}organisations/${id}/get-subscription-metadata/`, - ) - .catch(() => null), + getSubscriptionMetadata(getStore(), { id }), ] : [], ), diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index e0db01498636..622808159ec9 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -487,6 +487,8 @@ export type InviteLink = { export type SubscriptionMeta = { max_seats: number | null + audit_log_visibility_days: number | null + feature_history_visibility_days: number | null max_api_calls: number | null max_projects: number | null payment_source: string | null @@ -700,7 +702,7 @@ export type Res = { rolesPermissionUsers: PagedResponse createRolePermissionGroup: RolePermissionGroup rolePermissionGroup: PagedResponse - getSubscriptionMetadata: { id: string; max_api_calls: number } + subscriptionMetadata: SubscriptionMeta environment: Environment metadataModelFieldList: PagedResponse metadataModelField: MetadataModelField diff --git a/frontend/common/utils/utils.tsx b/frontend/common/utils/utils.tsx index 6fbb9f5f9bd4..7412795f691f 100644 --- a/frontend/common/utils/utils.tsx +++ b/frontend/common/utils/utils.tsx @@ -33,7 +33,8 @@ export type PaidFeature = | 'FORCE_2FA' | '4_EYES' | 'STALE_FLAGS' - | 'VERSIONING' + | 'VERSIONING_DAYS' + | 'AUDIT_DAYS' | 'AUTO_SEATS' | 'METADATA' | 'REALTIME' @@ -304,6 +305,9 @@ const Utils = Object.assign({}, require('./base/_utils'), { }, getNextPlan: (skipFree?: boolean) => { const currentPlan = Utils.getPlanName(AccountStore.getActiveOrgPlan()) + if (currentPlan !== planNames.enterprise && !Utils.isSaas()) { + return planNames.enterprise + } switch (currentPlan) { case planNames.free: { return skipFree ? planNames.startup : planNames.scaleUp diff --git a/frontend/web/components/AuditLog.tsx b/frontend/web/components/AuditLog.tsx index 9a530f01a1c9..d66651c7ec70 100644 --- a/frontend/web/components/AuditLog.tsx +++ b/frontend/web/components/AuditLog.tsx @@ -11,6 +11,10 @@ import PanelSearch from './PanelSearch' import JSONReference from './JSONReference' import moment from 'moment' import PlanBasedBanner from './PlanBasedAccess' +import { useGetSubscriptionMetadataQuery } from 'common/services/useSubscriptionMetadata' +import AccountStore from 'common/stores/account-store' +import { isVersionOverLimit } from 'common/services/useFeatureVersion' +import Tooltip from './Tooltip' type AuditLogType = { environmentId: string @@ -36,6 +40,9 @@ const AuditLog: FC = (props) => { setPage(1) }, ) + const { data: subscriptionMeta } = useGetSubscriptionMetadataQuery({ + id: AccountStore.getOrganisation()?.id, + }) const [environments, setEnvironments] = useState(props.environmentId) useEffect(() => { @@ -96,17 +103,49 @@ const AuditLog: FC = (props) => { }) const colour = index === -1 ? 0 : index let link: ReactNode = null - if ( - related_object_uuid && - related_object_type === 'EF_VERSION' && - environment - ) { + const date = moment(created_date) + const isVersionEvent = + related_object_uuid && related_object_type === 'EF_VERSION' && environment + const versionLimitDays = subscriptionMeta?.feature_history_visibility_days + + const isOverLimit = isVersionEvent + ? isVersionOverLimit(versionLimitDays, created_date) + : false + const VersionButton = ( + + ) + + if (isVersionEvent) { link = ( - + {isOverLimit ? ( + VersionButton + ) : ( + + {VersionButton} + + )} + + + } > - - + {isOverLimit + ? `
+ Unlock your feature's entire history.
Currently limited to${' '} + ${versionLimitDays} days. +
` + : ''} + ) } const inner = ( @@ -115,7 +154,7 @@ const AuditLog: FC = (props) => { className='table-column px-3 fs-small ln-sm' style={{ width: widths[0] }} > - {moment(created_date).format('Do MMM YYYY HH:mma')} + {date.format('Do MMM YYYY HH:mma')}
= (props) => { } const { env: envFilter } = Utils.fromParam() + const auditLimitDays = subscriptionMeta?.audit_log_visibility_days return ( - { - setSearchInput(Utils.safeParseEventValue(e)) - }} - paging={{ - ...(projectAuditLog || {}), - page, - pageSize: props.pageSize, - }} - nextPage={() => { - setPage(page + 1) - }} - prevPage={() => { - setPage(page - 1) - }} - goToPage={(page: number) => { - setPage(page) - }} - filterRow={() => true} - renderRow={renderRow} - header={ - -
- Date -
-
- User -
-
- Environment -
- Content -
- } - renderFooter={() => ( - + {!!auditLimitDays && ( + + Unlock your audit log history. Currently limited to{' '} + {auditLimitDays} days. +
+ } + theme={'description'} /> )} - renderNoResults={ - - You have no log messages for your project. - - } - /> + { + setSearchInput(Utils.safeParseEventValue(e)) + }} + paging={{ + ...(projectAuditLog || {}), + page, + pageSize: props.pageSize, + }} + nextPage={() => { + setPage(page + 1) + }} + prevPage={() => { + setPage(page - 1) + }} + goToPage={(page: number) => { + setPage(page) + }} + filterRow={() => true} + renderRow={renderRow} + header={ + +
+ Date +
+
+ User +
+
+ Environment +
+ Content +
+ } + renderFooter={() => ( + + )} + renderNoResults={ + + You have no log messages for your project. + + } + /> + ) } diff --git a/frontend/web/components/FeatureHistory.tsx b/frontend/web/components/FeatureHistory.tsx index 1e6bc12bff67..01b7b4240d9a 100644 --- a/frontend/web/components/FeatureHistory.tsx +++ b/frontend/web/components/FeatureHistory.tsx @@ -10,8 +10,9 @@ import InlineModal from './InlineModal' import TableFilterItem from './tables/TableFilterItem' import moment from 'moment' import DateList from './DateList' -import PlanBasedBanner from './PlanBasedAccess' import classNames from 'classnames' +import PlanBasedBanner from './PlanBasedAccess' +import { useGetSubscriptionMetadataQuery } from 'common/services/useSubscriptionMetadata' const widths = [250, 150] type FeatureHistoryPageType = { @@ -28,6 +29,12 @@ const FeatureHistory: FC = ({ projectId, }) => { const [open, setOpen] = useState(false) + const { data: subscriptionMeta } = useGetSubscriptionMetadataQuery({ + id: AccountStore.getOrganisation()?.id, + }) + const versionLimitDays = subscriptionMeta?.feature_history_visibility_days + + // @ts-ignore const { data: users } = useGetUsersQuery({ organisationId: AccountStore.getOrganisation().id, }) @@ -45,7 +52,6 @@ const FeatureHistory: FC = ({ const live = data?.results?.[0] const [compareToLive, setCompareToLive] = useState(false) const [diff, setDiff] = useState(null) - const versionLimit = 3 return (
Change History
@@ -54,30 +60,32 @@ const FeatureHistory: FC = ({ segment overrides.
- {/*{!!versionLimit && (*/} - {/* */} - {/*)}*/} + {!!versionLimitDays && ( + + Unlock your feature's entire history. Currently limited to{' '} + {versionLimitDays} days. +
+ } + theme={'page'} + /> + )} items={data} isLoading={isLoading} nextPage={() => setPage(page + 1)} prevPage={() => setPage(page + 1)} goToPage={setPage} + dateProperty={'live_from'} renderRow={(v: TFeatureVersion, i: number) => { - const isOverLimit = false const user = users?.find((user) => v.published_by === user.id) return ( - +
= { 'Add automatic stale flag detection, prompting your team to clean up old flags.', title: 'Stale Flag Detection', }, - 'VERSIONING': { + 'VERSIONING_DAYS': { description: 'Access all of your feature versions.', title: 'Version History', }, + 'AUDIT_DAYS': { + description: 'Access all of your audit logs.', + title: 'Audit Log History', + }, } -const PlanBasedBanner: FC = ({ - children, - className, - feature, - force, - theme, - withoutTooltip, -}) => { +const PlanBasedBanner: FC = ({ children, ...props }) => { + const { className, feature, force, theme, title } = props const trackFeature = () => API.trackEvent(Constants.events.VIEW_LOCKED_FEATURE(feature)) const hasPlan = !force && Utils.getPlansPermission(feature) @@ -182,7 +180,7 @@ const PlanBasedBanner: FC = ({ )} >
-
{featureDescriptions[feature].description}
+
{title || featureDescriptions[feature].description}
{ctas}
@@ -195,19 +193,9 @@ const PlanBasedBanner: FC = ({

{featureDescriptions[feature].title} - +

- +
) } diff --git a/frontend/web/styles/project/_buttons.scss b/frontend/web/styles/project/_buttons.scss index 2830600f9a92..01f2b32d8551 100644 --- a/frontend/web/styles/project/_buttons.scss +++ b/frontend/web/styles/project/_buttons.scss @@ -98,7 +98,7 @@ button.btn { } } &-tertiary { - color: $primary900; + color: $primary900 !important; background-color: $btn-tertiary-bg; box-shadow: 0 10px 20px rgba(247, 213, 110, .2); &:hover,