diff --git a/pkg/billing/plans.go b/pkg/billing/plans.go index b7eba1b2..338bf81c 100644 --- a/pkg/billing/plans.go +++ b/pkg/billing/plans.go @@ -52,6 +52,8 @@ func (p *basePlan) PriceIDs() (string, string) { return p.priceIDMonthl func (p *basePlan) TrialDays() int { return 14 } func (p *basePlan) RequestsLimit() int64 { return p.requestsLimit } func (p *basePlan) APIRequestsPerSecond() float64 { return p.apiRequestsPerSecond } +func (p *basePlan) PropertiesLimit() int { return 50 } +func (p *basePlan) OrgsLimit() int { return 10 } const ( version1 = 1 @@ -74,6 +76,8 @@ type Plan interface { CheckPropertiesLimit(count int) bool TrialDays() int RequestsLimit() int64 + PropertiesLimit() int + OrgsLimit() int APIRequestsPerSecond() float64 } diff --git a/pkg/db/business_impl.go b/pkg/db/business_impl.go index 15d70325..90fc0b5c 100644 --- a/pkg/db/business_impl.go +++ b/pkg/db/business_impl.go @@ -870,6 +870,7 @@ func (impl *BusinessStoreImpl) CreateNewProperty(ctx context.Context, params *db impl.cacheProperty(ctx, property) // invalidate org properties in cache as we just created a new property _ = impl.cache.Delete(ctx, orgPropertiesCacheKey(params.OrgID.Int32)) + _ = impl.cache.Delete(ctx, userPropertiesCountCacheKey(property.CreatorID.Int32)) auditEvent := newCreatePropertyAuditLogEvent(property, org) @@ -919,6 +920,7 @@ func (impl *BusinessStoreImpl) SoftDeleteProperty(ctx context.Context, prop *dbg _ = impl.cache.SetMissing(ctx, propertyByIDCacheKey(prop.ID)) // invalidate org properties in cache as we just deleted a property _ = impl.cache.Delete(ctx, orgPropertiesCacheKey(org.ID)) + _ = impl.cache.Delete(ctx, userPropertiesCountCacheKey(user.ID)) auditEvent := newDeletePropertyAuditLogEvent(prop, org, user) @@ -987,6 +989,7 @@ func (impl *BusinessStoreImpl) SoftDeleteOrganization(ctx context.Context, org * _ = impl.cache.SetMissing(ctx, orgCacheKey(org.ID)) // invalidate user orgs in cache as we just deleted one _ = impl.cache.Delete(ctx, userOrgsCacheKey(user.ID)) + _ = impl.cache.Delete(ctx, userPropertiesCountCacheKey(user.ID)) auditEvent := newOrgAuditLogEvent(user.ID, org, common.AuditLogActionSoftDelete) @@ -1688,6 +1691,11 @@ func (impl *BusinessStoreImpl) RetrieveUserPropertiesCount(ctx context.Context, return 0, ErrMaintenance } + cacheKey := userPropertiesCountCacheKey(userID) + if count, err := FetchCachedOne[int64](ctx, impl.cache, cacheKey); err == nil { + return *count, nil + } + count, err := impl.querier.GetUserPropertiesCount(ctx, Int(userID)) if err != nil { slog.ErrorContext(ctx, "Failed to retrieve user properties count", "userID", userID, common.ErrAttr(err)) @@ -1696,6 +1704,11 @@ func (impl *BusinessStoreImpl) RetrieveUserPropertiesCount(ctx context.Context, slog.DebugContext(ctx, "Fetched user properties count", "userID", userID, "count", count) + const propertiesCountTTL = 5 * time.Minute + c := new(int64) + *c = count + _ = impl.cache.SetWithTTL(ctx, cacheKey, c, propertiesCountTTL) + return count, nil } diff --git a/pkg/db/cache.go b/pkg/db/cache.go index e92752a6..24c36ad4 100644 --- a/pkg/db/cache.go +++ b/pkg/db/cache.go @@ -205,6 +205,7 @@ const ( userAuditLogsCacheKeyPrefix propertyAuditLogsCacheKeyPrefix orgAuditLogsCacheKeyPrefix + userPropertiesCountCachePrefix // Add new fields _above_ CACHE_KEY_PREFIXES_COUNT ) @@ -238,6 +239,7 @@ func init() { cachePrefixToStrings[userAuditLogsCacheKeyPrefix] = "userAuditLogs/" cachePrefixToStrings[propertyAuditLogsCacheKeyPrefix] = "propAuditLogs/" cachePrefixToStrings[orgAuditLogsCacheKeyPrefix] = "orgAuditLogs/" + cachePrefixToStrings[userPropertiesCountCachePrefix] = "userPropertiesCount/" for i, v := range cachePrefixToStrings { if len(v) == 0 { @@ -338,3 +340,6 @@ func propertyAuditLogsCacheKey(propID int32) CacheKey { func orgAuditLogsCacheKey(orgID int32) CacheKey { return CacheKey{Prefix: orgAuditLogsCacheKeyPrefix, IntValue: orgID} } +func userPropertiesCountCacheKey(userID int32) CacheKey { + return Int32CacheKey(userPropertiesCountCachePrefix, userID) +} diff --git a/pkg/portal/render_test.go b/pkg/portal/render_test.go index 8d8d4f2b..03f418e7 100644 --- a/pkg/portal/render_test.go +++ b/pkg/portal/render_test.go @@ -319,7 +319,11 @@ func TestRenderHTML(t *testing.T) { ActiveTabID: common.UsageEndpoint, Tabs: CreateTabViewModels(common.UsageEndpoint, server.SettingsTabs), }, - Limit: 12345, + OrgsCount: 2, + PropertiesCount: 10, + IncludedOrgsCount: 10, + IncludedPropertiesCount: 50, + Limit: 12345, }, selector: "", matches: []string{}, diff --git a/pkg/portal/settings.go b/pkg/portal/settings.go index e5173421..f785f1a1 100644 --- a/pkg/portal/settings.go +++ b/pkg/portal/settings.go @@ -66,7 +66,11 @@ type SettingsCommonRenderContext struct { type settingsUsageRenderContext struct { SettingsCommonRenderContext - Limit int + PropertiesCount int + OrgsCount int + IncludedPropertiesCount int + IncludedOrgsCount int + Limit int } type settingsGeneralRenderContext struct { @@ -720,6 +724,14 @@ func (s *Server) createUsageSettingsModel(ctx context.Context, user *dbgen.User) SettingsCommonRenderContext: s.CreateSettingsCommonRenderContext(common.UsageEndpoint, user), } + if orgs, err := s.Store.Impl().RetrieveUserOrganizations(ctx, user); err == nil { + renderCtx.OrgsCount = len(orgs) + } + + if count, err := s.Store.Impl().RetrieveUserPropertiesCount(ctx, user.ID); err == nil { + renderCtx.PropertiesCount = int(count) + } + if user.SubscriptionID.Valid { subscription, err := s.Store.Impl().RetrieveSubscription(ctx, user.SubscriptionID.Int32) if err != nil { @@ -729,6 +741,8 @@ func (s *Server) createUsageSettingsModel(ctx context.Context, user *dbgen.User) if plan, err := s.PlanService.FindPlan(subscription.ExternalProductID, subscription.ExternalPriceID, s.Stage, db.IsInternalSubscription(subscription.Source)); err == nil { renderCtx.Limit = int(plan.RequestsLimit()) + renderCtx.IncludedPropertiesCount = plan.PropertiesLimit() + renderCtx.IncludedOrgsCount = plan.OrgsLimit() } else { slog.ErrorContext(ctx, "Failed to find billing plan for usage tab", "productID", subscription.ExternalProductID, "priceID", subscription.ExternalPriceID, common.ErrAttr(err)) renderCtx.ErrorMessage = "Could not determine usage limits from your plan." diff --git a/web/layouts/settings-usage/content.html b/web/layouts/settings-usage/content.html index b9bcffa0..41d1d591 100644 --- a/web/layouts/settings-usage/content.html +++ b/web/layouts/settings-usage/content.html @@ -13,6 +13,23 @@
Monthly usage
+