mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 02:28:59 +00:00
Encode canonical self-hosted feature metadata
This commit is contained in:
parent
8b6a429186
commit
b4692ef7f0
13 changed files with 1244 additions and 262 deletions
|
|
@ -96,6 +96,9 @@ agreement, and cloud-specific enforcement rules.
|
|||
74. `pkg/licensing/trial_activation_public_key_override_dev.go`
|
||||
75. `pkg/licensing/trial_activation_public_key_override_release.go`
|
||||
76. `pkg/licensing/testing_helpers.go`
|
||||
77. `pkg/licensing/self_hosted_feature_catalog.go`
|
||||
78. `frontend-modern/src/utils/selfHostedFeatureCatalog.generated.ts`
|
||||
79. `pulse-pro:license-server/self_hosted_feature_catalog.generated.go`
|
||||
|
||||
## Shared Boundaries
|
||||
|
||||
|
|
@ -298,6 +301,16 @@ Community limit enforcement.
|
|||
`incident memory`, `scheduled remediations`, and `execution audit trail`
|
||||
must likewise stay out of current v6 commercial copy unless Pulse ships a
|
||||
first-class product surface that makes those names truthful again.
|
||||
That same self-hosted pricing boundary now also owns canonical feature
|
||||
classification metadata. `pkg/licensing/self_hosted_feature_catalog.go`
|
||||
is the source of truth for customer-facing self-hosted feature labels,
|
||||
comparison visibility, plan roles (`primary_pillar`, `included_extra`,
|
||||
`compatibility_only`, and hidden/included states), and generic upgrade
|
||||
reasons. `frontend-modern/src/utils/selfHostedFeatureCatalog.generated.ts`
|
||||
and `pulse-pro:license-server/self_hosted_feature_catalog.generated.go`
|
||||
are generated projections of that source for app and public-pricing
|
||||
consumers; those projections must not drift through hand-maintained
|
||||
parallel lists or page-local renaming.
|
||||
26. Keep hosted trial-activation verifier source selection compile-time owned.
|
||||
`pkg/licensing/trial_activation.go`,
|
||||
`pkg/licensing/trial_activation_public_key_override_dev.go`, and
|
||||
|
|
|
|||
|
|
@ -1710,6 +1710,7 @@
|
|||
"frontend-modern/src/utils/licensePresentation.ts",
|
||||
"frontend-modern/src/utils/monitoredSystemPresentation.ts",
|
||||
"frontend-modern/src/utils/pricingHandoff.ts",
|
||||
"frontend-modern/src/utils/selfHostedFeatureCatalog.generated.ts",
|
||||
"frontend-modern/src/utils/selfHostedPlans.ts",
|
||||
"frontend-modern/src/utils/upgradePresentation.ts",
|
||||
"internal/api/public_signup_handlers.go",
|
||||
|
|
@ -1731,6 +1732,7 @@
|
|||
"pulse-pro:license-server/checkout_funnel.go",
|
||||
"pulse-pro:license-server/grandfathering.go",
|
||||
"pulse-pro:license-server/main.go",
|
||||
"pulse-pro:license-server/self_hosted_feature_catalog.generated.go",
|
||||
"pulse-pro:license-server/v6_checkout.go",
|
||||
"pulse-pro:license-server/v6_handlers.go",
|
||||
"pulse-pro:scripts/audit_v6_preview_demo.sh",
|
||||
|
|
@ -1798,6 +1800,24 @@
|
|||
"pkg/licensing/grant_claims_contract_test.go"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "self-hosted-feature-catalog",
|
||||
"label": "self-hosted feature catalog proof",
|
||||
"match_prefixes": [],
|
||||
"match_files": [
|
||||
"frontend-modern/src/utils/selfHostedFeatureCatalog.generated.ts",
|
||||
"pkg/licensing/self_hosted_feature_catalog.go",
|
||||
"pulse-pro:license-server/self_hosted_feature_catalog.generated.go"
|
||||
],
|
||||
"allow_same_subsystem_tests": false,
|
||||
"test_prefixes": [],
|
||||
"exact_files": [
|
||||
"frontend-modern/src/utils/__tests__/licensePresentation.test.ts",
|
||||
"frontend-modern/src/utils/__tests__/selfHostedPlans.test.ts",
|
||||
"pkg/licensing/features_test.go",
|
||||
"pkg/licensing/upgrade_test.go"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "stripe-plan-derivation",
|
||||
"label": "stripe plan derivation proof",
|
||||
|
|
|
|||
|
|
@ -118,6 +118,8 @@ describe('licensePresentation', () => {
|
|||
it('returns canonical feature labels', () => {
|
||||
expect(getLicenseFeatureLabel('ai_patrol')).toBe('Pulse Patrol');
|
||||
expect(getLicenseFeatureLabel('mobile_app')).toBe('Mobile App Access');
|
||||
expect(getLicenseFeatureLabel('update_alerts')).toBe('Update Alerts');
|
||||
expect(getLicenseFeatureLabel('relay')).toBe('Pulse Relay (Remote Access)');
|
||||
expect(getLicenseFeatureLabel('custom_feature')).toBe('Custom Feature');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,26 @@ describe('selfHostedPlans', () => {
|
|||
relay: 'Unlimited',
|
||||
pro: 'Unlimited',
|
||||
});
|
||||
|
||||
expect(SELF_HOSTED_FEATURE_ROWS).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
key: 'update_alerts',
|
||||
name: 'Update Alerts',
|
||||
community: true,
|
||||
relay: true,
|
||||
pro: true,
|
||||
},
|
||||
{
|
||||
key: 'relay',
|
||||
name: 'Pulse Relay (Remote Access)',
|
||||
community: false,
|
||||
relay: true,
|
||||
pro: true,
|
||||
},
|
||||
]),
|
||||
);
|
||||
expect(SELF_HOSTED_FEATURE_ROWS.find((row) => row.key === 'kubernetes_ai')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('keeps shared self-hosted commercial copy in the common contract', () => {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ import {
|
|||
type MonitoredSystemLimitUsageStatus,
|
||||
} from '@/utils/monitoredSystemPresentation';
|
||||
import { getSelfHostedPlanDefinitionForBillingTier } from '@/utils/selfHostedPlans';
|
||||
import {
|
||||
getSelfHostedFeatureCatalogEntry,
|
||||
isDisplayableSelfHostedFeatureKey,
|
||||
} from '@/utils/selfHostedFeatureCatalog.generated';
|
||||
import { titleCaseDelimitedLabel } from '@/utils/textPresentation';
|
||||
|
||||
const TIER_LABELS: Record<string, string> = {
|
||||
|
|
@ -33,28 +37,6 @@ const SELF_HOSTED_PLAN_LABELS: Record<string, string> = {
|
|||
pro: 'Pulse Pro',
|
||||
};
|
||||
|
||||
const FEATURE_LABELS: Record<string, string> = {
|
||||
ai_patrol: 'Pulse Patrol',
|
||||
ai_alerts: 'Pulse Alert Analysis',
|
||||
ai_autofix: 'Patrol Auto-Fix',
|
||||
kubernetes_ai: 'Kubernetes AI Analysis (Compatibility)',
|
||||
update_alerts: 'Update Alerts',
|
||||
sso: 'Basic SSO (OIDC)',
|
||||
advanced_sso: 'Advanced SSO (SAML/Multi-Provider)',
|
||||
rbac: 'Role-Based Access Control (RBAC)',
|
||||
audit_logging: 'Audit Logging',
|
||||
advanced_reporting: 'PDF/CSV Reporting',
|
||||
agent_profiles: 'Centralized Agent Profiles',
|
||||
relay: 'Pulse Relay (Remote Access)',
|
||||
mobile_app: 'Mobile App Access',
|
||||
push_notifications: 'Push Notifications',
|
||||
long_term_metrics: 'Extended Metric History',
|
||||
multi_user: 'Multi-User Mode',
|
||||
white_label: 'White-Label Branding',
|
||||
multi_tenant: 'Multi-Tenant Mode',
|
||||
unlimited: 'Unlimited Instances',
|
||||
};
|
||||
|
||||
const FEATURE_MIN_TIER_LABELS: Record<string, string> = {
|
||||
relay: 'Relay',
|
||||
mobile_app: 'Relay',
|
||||
|
|
@ -62,13 +44,6 @@ const FEATURE_MIN_TIER_LABELS: Record<string, string> = {
|
|||
multi_tenant: 'MSP',
|
||||
};
|
||||
|
||||
const NON_DISPLAYABLE_FEATURES = new Set([
|
||||
'kubernetes_ai',
|
||||
'multi_user',
|
||||
'white_label',
|
||||
'unlimited',
|
||||
]);
|
||||
|
||||
export interface LicenseSubscriptionStatusPresentation {
|
||||
label: string;
|
||||
badgeClass: string;
|
||||
|
|
@ -179,13 +154,14 @@ export const getSelfHostedPlanLabel = (tier?: string | null): string => {
|
|||
export const getLicenseFeatureLabel = (feature?: string | null): string => {
|
||||
const normalized = (feature || '').trim().toLowerCase();
|
||||
if (!normalized) return 'Unknown';
|
||||
return FEATURE_LABELS[normalized] || titleCaseDelimitedLabel(normalized);
|
||||
const entry = getSelfHostedFeatureCatalogEntry(normalized);
|
||||
return entry?.comparisonName || titleCaseDelimitedLabel(normalized);
|
||||
};
|
||||
|
||||
export const isDisplayableLicenseFeature = (feature?: string | null): boolean => {
|
||||
const normalized = (feature || '').trim().toLowerCase();
|
||||
if (!normalized) return false;
|
||||
return !NON_DISPLAYABLE_FEATURES.has(normalized);
|
||||
return isDisplayableSelfHostedFeatureKey(normalized);
|
||||
};
|
||||
|
||||
export const getFeatureMinTierLabel = (feature?: string | null): string => {
|
||||
|
|
|
|||
399
frontend-modern/src/utils/selfHostedFeatureCatalog.generated.ts
Normal file
399
frontend-modern/src/utils/selfHostedFeatureCatalog.generated.ts
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
// This file is generated by scripts/generate-self-hosted-feature-catalog.go.
|
||||
// Do not edit it manually.
|
||||
|
||||
export type SelfHostedFeatureRole =
|
||||
| 'hidden'
|
||||
| 'included'
|
||||
| 'primary_pillar'
|
||||
| 'included_extra'
|
||||
| 'compatibility_only';
|
||||
|
||||
export interface GeneratedSelfHostedFeatureCatalogEntry {
|
||||
key: string;
|
||||
displayName: string;
|
||||
comparisonName: string;
|
||||
showInComparisonTable: boolean;
|
||||
displayableInSelfHostedPlan: boolean;
|
||||
roles: {
|
||||
community: SelfHostedFeatureRole;
|
||||
relay: SelfHostedFeatureRole;
|
||||
pro: SelfHostedFeatureRole;
|
||||
};
|
||||
includedIn: {
|
||||
community: boolean;
|
||||
relay: boolean;
|
||||
pro: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const SELF_HOSTED_FEATURE_CATALOG: readonly GeneratedSelfHostedFeatureCatalogEntry[] = [
|
||||
{
|
||||
"key": "update_alerts",
|
||||
"displayName": "Update Alerts (Container/Package Updates)",
|
||||
"comparisonName": "Update Alerts",
|
||||
"showInComparisonTable": true,
|
||||
"displayableInSelfHostedPlan": true,
|
||||
"roles": {
|
||||
"community": "included",
|
||||
"relay": "included",
|
||||
"pro": "included"
|
||||
},
|
||||
"includedIn": {
|
||||
"community": true,
|
||||
"relay": true,
|
||||
"pro": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "sso",
|
||||
"displayName": "Basic SSO (OIDC)",
|
||||
"comparisonName": "Basic SSO (OIDC)",
|
||||
"showInComparisonTable": true,
|
||||
"displayableInSelfHostedPlan": true,
|
||||
"roles": {
|
||||
"community": "included",
|
||||
"relay": "included",
|
||||
"pro": "included"
|
||||
},
|
||||
"includedIn": {
|
||||
"community": true,
|
||||
"relay": true,
|
||||
"pro": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "ai_patrol",
|
||||
"displayName": "Pulse Patrol (Background Health Checks)",
|
||||
"comparisonName": "Pulse Patrol",
|
||||
"showInComparisonTable": true,
|
||||
"displayableInSelfHostedPlan": true,
|
||||
"roles": {
|
||||
"community": "included",
|
||||
"relay": "included",
|
||||
"pro": "included"
|
||||
},
|
||||
"includedIn": {
|
||||
"community": true,
|
||||
"relay": true,
|
||||
"pro": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "relay",
|
||||
"displayName": "Pulse Relay (Remote Access)",
|
||||
"comparisonName": "Pulse Relay (Remote Access)",
|
||||
"showInComparisonTable": true,
|
||||
"displayableInSelfHostedPlan": true,
|
||||
"roles": {
|
||||
"community": "hidden",
|
||||
"relay": "primary_pillar",
|
||||
"pro": "included"
|
||||
},
|
||||
"includedIn": {
|
||||
"community": false,
|
||||
"relay": true,
|
||||
"pro": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "mobile_app",
|
||||
"displayName": "Mobile App Access",
|
||||
"comparisonName": "Mobile App Access",
|
||||
"showInComparisonTable": true,
|
||||
"displayableInSelfHostedPlan": true,
|
||||
"roles": {
|
||||
"community": "hidden",
|
||||
"relay": "primary_pillar",
|
||||
"pro": "included"
|
||||
},
|
||||
"includedIn": {
|
||||
"community": false,
|
||||
"relay": true,
|
||||
"pro": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "push_notifications",
|
||||
"displayName": "Push Notifications",
|
||||
"comparisonName": "Push Notifications",
|
||||
"showInComparisonTable": true,
|
||||
"displayableInSelfHostedPlan": true,
|
||||
"roles": {
|
||||
"community": "hidden",
|
||||
"relay": "primary_pillar",
|
||||
"pro": "included"
|
||||
},
|
||||
"includedIn": {
|
||||
"community": false,
|
||||
"relay": true,
|
||||
"pro": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "ai_alerts",
|
||||
"displayName": "Alert Analysis",
|
||||
"comparisonName": "Pulse Alert Analysis",
|
||||
"showInComparisonTable": true,
|
||||
"displayableInSelfHostedPlan": true,
|
||||
"roles": {
|
||||
"community": "hidden",
|
||||
"relay": "hidden",
|
||||
"pro": "primary_pillar"
|
||||
},
|
||||
"includedIn": {
|
||||
"community": false,
|
||||
"relay": false,
|
||||
"pro": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "ai_autofix",
|
||||
"displayName": "Pulse Patrol Auto-Fix",
|
||||
"comparisonName": "Patrol Auto-Fix",
|
||||
"showInComparisonTable": true,
|
||||
"displayableInSelfHostedPlan": true,
|
||||
"roles": {
|
||||
"community": "hidden",
|
||||
"relay": "hidden",
|
||||
"pro": "primary_pillar"
|
||||
},
|
||||
"includedIn": {
|
||||
"community": false,
|
||||
"relay": false,
|
||||
"pro": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "long_term_metrics",
|
||||
"displayName": "Extended Metric History",
|
||||
"comparisonName": "Extended Metric History",
|
||||
"showInComparisonTable": false,
|
||||
"displayableInSelfHostedPlan": true,
|
||||
"roles": {
|
||||
"community": "hidden",
|
||||
"relay": "primary_pillar",
|
||||
"pro": "primary_pillar"
|
||||
},
|
||||
"includedIn": {
|
||||
"community": false,
|
||||
"relay": true,
|
||||
"pro": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "advanced_sso",
|
||||
"displayName": "Advanced SSO (SAML/Multi-Provider)",
|
||||
"comparisonName": "Advanced SSO (SAML/Multi-Provider)",
|
||||
"showInComparisonTable": true,
|
||||
"displayableInSelfHostedPlan": true,
|
||||
"roles": {
|
||||
"community": "hidden",
|
||||
"relay": "hidden",
|
||||
"pro": "included_extra"
|
||||
},
|
||||
"includedIn": {
|
||||
"community": false,
|
||||
"relay": false,
|
||||
"pro": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "rbac",
|
||||
"displayName": "Role-Based Access Control (RBAC)",
|
||||
"comparisonName": "Role-Based Access Control (RBAC)",
|
||||
"showInComparisonTable": true,
|
||||
"displayableInSelfHostedPlan": true,
|
||||
"roles": {
|
||||
"community": "hidden",
|
||||
"relay": "hidden",
|
||||
"pro": "included_extra"
|
||||
},
|
||||
"includedIn": {
|
||||
"community": false,
|
||||
"relay": false,
|
||||
"pro": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "audit_logging",
|
||||
"displayName": "Audit Logging",
|
||||
"comparisonName": "Audit Logging",
|
||||
"showInComparisonTable": true,
|
||||
"displayableInSelfHostedPlan": true,
|
||||
"roles": {
|
||||
"community": "hidden",
|
||||
"relay": "hidden",
|
||||
"pro": "included_extra"
|
||||
},
|
||||
"includedIn": {
|
||||
"community": false,
|
||||
"relay": false,
|
||||
"pro": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "advanced_reporting",
|
||||
"displayName": "PDF/CSV Reporting",
|
||||
"comparisonName": "PDF/CSV Reporting",
|
||||
"showInComparisonTable": true,
|
||||
"displayableInSelfHostedPlan": true,
|
||||
"roles": {
|
||||
"community": "hidden",
|
||||
"relay": "hidden",
|
||||
"pro": "included_extra"
|
||||
},
|
||||
"includedIn": {
|
||||
"community": false,
|
||||
"relay": false,
|
||||
"pro": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "agent_profiles",
|
||||
"displayName": "Centralized Agent Profiles",
|
||||
"comparisonName": "Centralized Agent Profiles",
|
||||
"showInComparisonTable": true,
|
||||
"displayableInSelfHostedPlan": true,
|
||||
"roles": {
|
||||
"community": "hidden",
|
||||
"relay": "hidden",
|
||||
"pro": "included_extra"
|
||||
},
|
||||
"includedIn": {
|
||||
"community": false,
|
||||
"relay": false,
|
||||
"pro": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "kubernetes_ai",
|
||||
"displayName": "Kubernetes AI Analysis (Compatibility)",
|
||||
"comparisonName": "Kubernetes AI Analysis (Compatibility)",
|
||||
"showInComparisonTable": false,
|
||||
"displayableInSelfHostedPlan": false,
|
||||
"roles": {
|
||||
"community": "hidden",
|
||||
"relay": "hidden",
|
||||
"pro": "compatibility_only"
|
||||
},
|
||||
"includedIn": {
|
||||
"community": false,
|
||||
"relay": false,
|
||||
"pro": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "multi_user",
|
||||
"displayName": "Multi-User Mode",
|
||||
"comparisonName": "Multi-User Mode",
|
||||
"showInComparisonTable": false,
|
||||
"displayableInSelfHostedPlan": false,
|
||||
"roles": {
|
||||
"community": "hidden",
|
||||
"relay": "hidden",
|
||||
"pro": "hidden"
|
||||
},
|
||||
"includedIn": {
|
||||
"community": false,
|
||||
"relay": false,
|
||||
"pro": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "white_label",
|
||||
"displayName": "White-Label Branding",
|
||||
"comparisonName": "White-Label Branding",
|
||||
"showInComparisonTable": false,
|
||||
"displayableInSelfHostedPlan": false,
|
||||
"roles": {
|
||||
"community": "hidden",
|
||||
"relay": "hidden",
|
||||
"pro": "hidden"
|
||||
},
|
||||
"includedIn": {
|
||||
"community": false,
|
||||
"relay": false,
|
||||
"pro": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "multi_tenant",
|
||||
"displayName": "Multi-Tenant Mode",
|
||||
"comparisonName": "Multi-Tenant Mode",
|
||||
"showInComparisonTable": false,
|
||||
"displayableInSelfHostedPlan": true,
|
||||
"roles": {
|
||||
"community": "hidden",
|
||||
"relay": "hidden",
|
||||
"pro": "hidden"
|
||||
},
|
||||
"includedIn": {
|
||||
"community": false,
|
||||
"relay": false,
|
||||
"pro": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "unlimited",
|
||||
"displayName": "Unlimited Instances",
|
||||
"comparisonName": "Unlimited Instances",
|
||||
"showInComparisonTable": false,
|
||||
"displayableInSelfHostedPlan": false,
|
||||
"roles": {
|
||||
"community": "hidden",
|
||||
"relay": "hidden",
|
||||
"pro": "hidden"
|
||||
},
|
||||
"includedIn": {
|
||||
"community": false,
|
||||
"relay": false,
|
||||
"pro": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "demo_fixtures",
|
||||
"displayName": "Demo Fixtures (Internal)",
|
||||
"comparisonName": "Demo Fixtures (Internal)",
|
||||
"showInComparisonTable": false,
|
||||
"displayableInSelfHostedPlan": false,
|
||||
"roles": {
|
||||
"community": "hidden",
|
||||
"relay": "hidden",
|
||||
"pro": "hidden"
|
||||
},
|
||||
"includedIn": {
|
||||
"community": false,
|
||||
"relay": false,
|
||||
"pro": false
|
||||
}
|
||||
}
|
||||
] as const;
|
||||
|
||||
export const SELF_HOSTED_FEATURE_CATALOG_BY_KEY: Record<string, GeneratedSelfHostedFeatureCatalogEntry> = Object.fromEntries(
|
||||
SELF_HOSTED_FEATURE_CATALOG.map((entry) => [entry.key, entry]),
|
||||
) as Record<string, GeneratedSelfHostedFeatureCatalogEntry>;
|
||||
|
||||
export function getSelfHostedFeatureCatalogEntry(
|
||||
key?: string | null,
|
||||
): GeneratedSelfHostedFeatureCatalogEntry | null {
|
||||
const normalized = (key || '').trim();
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
return SELF_HOSTED_FEATURE_CATALOG_BY_KEY[normalized] || null;
|
||||
}
|
||||
|
||||
export function isDisplayableSelfHostedFeatureKey(key?: string | null): boolean {
|
||||
const entry = getSelfHostedFeatureCatalogEntry(key);
|
||||
if (!entry) {
|
||||
return true;
|
||||
}
|
||||
return entry.displayableInSelfHostedPlan;
|
||||
}
|
||||
|
||||
export function getSelfHostedFeaturesForRole(
|
||||
tier: 'community' | 'relay' | 'pro',
|
||||
role: SelfHostedFeatureRole,
|
||||
): readonly GeneratedSelfHostedFeatureCatalogEntry[] {
|
||||
return SELF_HOSTED_FEATURE_CATALOG.filter((entry) => entry.roles[tier] === role);
|
||||
}
|
||||
|
|
@ -1,3 +1,8 @@
|
|||
import {
|
||||
SELF_HOSTED_FEATURE_CATALOG,
|
||||
getSelfHostedFeaturesForRole,
|
||||
} from './selfHostedFeatureCatalog.generated';
|
||||
|
||||
export type SelfHostedTierKey = 'community' | 'relay' | 'pro';
|
||||
|
||||
export interface SelfHostedPlanDefinition {
|
||||
|
|
@ -42,6 +47,35 @@ export interface SelfHostedCommercialPresentation {
|
|||
footerLinks: readonly SelfHostedLinkCTA[];
|
||||
}
|
||||
|
||||
const getTierMetricHistoryHighlight = (tier: SelfHostedTierKey, metricHistoryDays: number) =>
|
||||
tier === 'relay' || tier === 'pro' ? `${metricHistoryDays}-day metric history` : null;
|
||||
|
||||
const getTierEntitlementHighlights = (
|
||||
tier: SelfHostedTierKey,
|
||||
metricHistoryDays: number,
|
||||
): readonly string[] => {
|
||||
const highlights = getSelfHostedFeaturesForRole(tier, 'primary_pillar').map((entry) =>
|
||||
entry.key === 'long_term_metrics' ? `${metricHistoryDays}-day metric history` : entry.comparisonName,
|
||||
);
|
||||
const metricHistoryHighlight = getTierMetricHistoryHighlight(tier, metricHistoryDays);
|
||||
if (!metricHistoryHighlight || highlights.includes(metricHistoryHighlight)) {
|
||||
return highlights;
|
||||
}
|
||||
return [...highlights, metricHistoryHighlight];
|
||||
};
|
||||
|
||||
const getTierIncludedExtras = (tier: SelfHostedTierKey): readonly string[] =>
|
||||
getSelfHostedFeaturesForRole(tier, 'included_extra').map((entry) => entry.displayName);
|
||||
|
||||
const buildSelfHostedComparisonFeatureRows = (): readonly SelfHostedFeatureRow[] =>
|
||||
SELF_HOSTED_FEATURE_CATALOG.filter((entry) => entry.showInComparisonTable).map((entry) => ({
|
||||
key: entry.key,
|
||||
name: entry.comparisonName,
|
||||
community: entry.includedIn.community,
|
||||
relay: entry.includedIn.relay,
|
||||
pro: entry.includedIn.pro,
|
||||
}));
|
||||
|
||||
export const SELF_HOSTED_PLAN_DEFINITIONS: readonly SelfHostedPlanDefinition[] = [
|
||||
{
|
||||
tier: 'community',
|
||||
|
|
@ -80,12 +114,7 @@ export const SELF_HOSTED_PLAN_DEFINITIONS: readonly SelfHostedPlanDefinition[] =
|
|||
billingExtrasSummary: 'Remote access, mobile, and push',
|
||||
entitlementSummary:
|
||||
'Relay is active on this instance. Remote access, mobile, push, and longer history are unlocked right now.',
|
||||
entitlementHighlights: [
|
||||
'Pulse Relay (Remote Access)',
|
||||
'Mobile App Access',
|
||||
'Push Notifications',
|
||||
'14-day metric history',
|
||||
],
|
||||
entitlementHighlights: getTierEntitlementHighlights('relay', 14),
|
||||
includedExtras: [],
|
||||
comparisonSummary:
|
||||
'Reach this Pulse instance securely from anywhere, check it from mobile, get push notifications, and keep 14 days of history.',
|
||||
|
|
@ -107,18 +136,8 @@ export const SELF_HOSTED_PLAN_DEFINITIONS: readonly SelfHostedPlanDefinition[] =
|
|||
billingExtrasSummary: 'Root-cause analysis, remediation, and admin extras',
|
||||
entitlementSummary:
|
||||
'Pulse Pro is active on this instance. Root-cause analysis, safe remediation, and 90-day history are unlocked right now.',
|
||||
entitlementHighlights: [
|
||||
'Pulse Alert Analysis',
|
||||
'Patrol Auto-Fix',
|
||||
'90-day metric history',
|
||||
],
|
||||
includedExtras: [
|
||||
'Advanced SSO (SAML/Multi-Provider)',
|
||||
'Role-Based Access Control (RBAC)',
|
||||
'Audit Logging',
|
||||
'PDF/CSV Reporting',
|
||||
'Centralized Agent Profiles',
|
||||
],
|
||||
entitlementHighlights: getTierEntitlementHighlights('pro', 90),
|
||||
includedExtras: getTierIncludedExtras('pro'),
|
||||
comparisonSummary:
|
||||
'Move from monitoring into operations with root-cause answers, safe remediation, and 90-day history. Pulse Pro also includes SAML SSO, RBAC, audit logging, reporting, and agent profiles.',
|
||||
highlights: [
|
||||
|
|
@ -203,95 +222,5 @@ export const SELF_HOSTED_FEATURE_ROWS: readonly SelfHostedFeatureRow[] = [
|
|||
relay: `${SELF_HOSTED_PLAN_BY_TIER.relay.metricHistoryDays} days`,
|
||||
pro: `${SELF_HOSTED_PLAN_BY_TIER.pro.metricHistoryDays} days`,
|
||||
},
|
||||
{
|
||||
key: 'update_alerts',
|
||||
name: 'Update Alerts (Container/Package)',
|
||||
community: true,
|
||||
relay: true,
|
||||
pro: true,
|
||||
},
|
||||
{
|
||||
key: 'sso',
|
||||
name: 'Basic SSO (OIDC)',
|
||||
community: true,
|
||||
relay: true,
|
||||
pro: true,
|
||||
},
|
||||
{
|
||||
key: 'relay',
|
||||
name: 'Remote Access (Relay)',
|
||||
community: false,
|
||||
relay: true,
|
||||
pro: true,
|
||||
},
|
||||
{
|
||||
key: 'mobile_app',
|
||||
name: 'Mobile App Access',
|
||||
community: false,
|
||||
relay: true,
|
||||
pro: true,
|
||||
},
|
||||
{
|
||||
key: 'push_notifications',
|
||||
name: 'Push Notifications',
|
||||
community: false,
|
||||
relay: true,
|
||||
pro: true,
|
||||
},
|
||||
{
|
||||
key: 'ai_patrol',
|
||||
name: 'Pulse Patrol',
|
||||
community: true,
|
||||
relay: true,
|
||||
pro: true,
|
||||
},
|
||||
{
|
||||
key: 'ai_autofix',
|
||||
name: 'Patrol Auto-Fix',
|
||||
community: false,
|
||||
relay: false,
|
||||
pro: true,
|
||||
},
|
||||
{
|
||||
key: 'ai_alerts',
|
||||
name: 'Pulse Alert Analysis',
|
||||
community: false,
|
||||
relay: false,
|
||||
pro: true,
|
||||
},
|
||||
{
|
||||
key: 'rbac',
|
||||
name: 'Role-Based Access Control (RBAC)',
|
||||
community: false,
|
||||
relay: false,
|
||||
pro: true,
|
||||
},
|
||||
{
|
||||
key: 'audit_logging',
|
||||
name: 'Audit Logging',
|
||||
community: false,
|
||||
relay: false,
|
||||
pro: true,
|
||||
},
|
||||
{
|
||||
key: 'advanced_sso',
|
||||
name: 'Advanced SSO (SAML/Multi-Provider)',
|
||||
community: false,
|
||||
relay: false,
|
||||
pro: true,
|
||||
},
|
||||
{
|
||||
key: 'agent_profiles',
|
||||
name: 'Centralized Agent Profiles',
|
||||
community: false,
|
||||
relay: false,
|
||||
pro: true,
|
||||
},
|
||||
{
|
||||
key: 'advanced_reporting',
|
||||
name: 'PDF/CSV Reporting',
|
||||
community: false,
|
||||
relay: false,
|
||||
pro: true,
|
||||
},
|
||||
...buildSelfHostedComparisonFeatureRows(),
|
||||
] as const;
|
||||
|
|
|
|||
|
|
@ -458,60 +458,8 @@ func GetFeatureMinTierName(feature string) string {
|
|||
|
||||
// GetFeatureDisplayName returns a human-readable name for a feature.
|
||||
func GetFeatureDisplayName(feature string) string {
|
||||
switch feature {
|
||||
case FeatureAIPatrol:
|
||||
return "Pulse Patrol (Background Health Checks)"
|
||||
case FeatureAIAlerts:
|
||||
return "Alert Analysis"
|
||||
case FeatureAIAutoFix:
|
||||
return "Pulse Patrol Auto-Fix"
|
||||
case FeatureKubernetesAI:
|
||||
return "Kubernetes AI Analysis (Compatibility)"
|
||||
case FeatureUpdateAlerts:
|
||||
return "Update Alerts (Container/Package Updates)"
|
||||
case FeatureRBAC:
|
||||
return "Role-Based Access Control (RBAC)"
|
||||
case FeatureMultiUser:
|
||||
return "Multi-User Mode"
|
||||
case FeatureWhiteLabel:
|
||||
return "White-Label Branding"
|
||||
case FeatureMultiTenant:
|
||||
return "Multi-Tenant Mode"
|
||||
case FeatureUnlimited:
|
||||
return "Unlimited Instances"
|
||||
case FeatureDemoFixtures:
|
||||
return "Demo Fixtures (Internal)"
|
||||
case FeatureAgentProfiles:
|
||||
return "Centralized Agent Profiles"
|
||||
case FeatureAuditLogging:
|
||||
return "Audit Logging"
|
||||
case FeatureSSO:
|
||||
return "Basic SSO (OIDC)"
|
||||
case FeatureAdvancedSSO:
|
||||
return "Advanced SSO (SAML/Multi-Provider)"
|
||||
case FeatureRelay:
|
||||
return "Pulse Relay (Remote Access)"
|
||||
case FeatureMobileApp:
|
||||
return "Mobile App Access"
|
||||
case FeaturePushNotifications:
|
||||
return "Push Notifications"
|
||||
case FeatureAdvancedReporting:
|
||||
return "PDF/CSV Reporting"
|
||||
case FeatureLongTermMetrics:
|
||||
return "Extended Metric History"
|
||||
default:
|
||||
return feature
|
||||
if entry, ok := GetFeatureMetadata(feature); ok {
|
||||
return entry.DisplayName
|
||||
}
|
||||
}
|
||||
|
||||
var compatibilityOnlyFeatures = map[string]struct{}{
|
||||
FeatureKubernetesAI: {},
|
||||
}
|
||||
|
||||
// IsCompatibilityOnlyFeature reports capability keys that remain valid runtime
|
||||
// contracts for backwards compatibility but should not be marketed as current
|
||||
// v6 commercial pillars or generic upgrade prompts.
|
||||
func IsCompatibilityOnlyFeature(feature string) bool {
|
||||
_, ok := compatibilityOnlyFeatures[feature]
|
||||
return ok
|
||||
return feature
|
||||
}
|
||||
|
|
|
|||
|
|
@ -264,6 +264,46 @@ func TestIsCompatibilityOnlyFeature(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSelfHostedFeatureMetadataKeepsCanonicalPlanLabelsAndVisibility(t *testing.T) {
|
||||
updateAlerts, ok := GetFeatureMetadata(FeatureUpdateAlerts)
|
||||
if !ok {
|
||||
t.Fatalf("expected metadata for %q", FeatureUpdateAlerts)
|
||||
}
|
||||
if updateAlerts.DisplayName != "Update Alerts (Container/Package Updates)" {
|
||||
t.Fatalf("DisplayName = %q, want detailed runtime label", updateAlerts.DisplayName)
|
||||
}
|
||||
if updateAlerts.ComparisonName != "Update Alerts" {
|
||||
t.Fatalf("ComparisonName = %q, want customer-facing label", updateAlerts.ComparisonName)
|
||||
}
|
||||
|
||||
relay, ok := GetFeatureMetadata(FeatureRelay)
|
||||
if !ok {
|
||||
t.Fatalf("expected metadata for %q", FeatureRelay)
|
||||
}
|
||||
if relay.ComparisonName != "Pulse Relay (Remote Access)" {
|
||||
t.Fatalf("ComparisonName = %q, want canonical Relay marketing label", relay.ComparisonName)
|
||||
}
|
||||
if GetSelfHostedFeatureRole(FeatureRelay, TierRelay) != SelfHostedFeatureRolePrimaryPillar {
|
||||
t.Fatalf("expected %q to stay a Relay primary pillar", FeatureRelay)
|
||||
}
|
||||
|
||||
multiTenant, ok := GetFeatureMetadata(FeatureMultiTenant)
|
||||
if !ok {
|
||||
t.Fatalf("expected metadata for %q", FeatureMultiTenant)
|
||||
}
|
||||
if !multiTenant.DisplayableInPlanUI {
|
||||
t.Fatalf("expected %q to remain customer-displayable for MSP/Enterprise surfaces", FeatureMultiTenant)
|
||||
}
|
||||
|
||||
kubernetes, ok := GetFeatureMetadata(FeatureKubernetesAI)
|
||||
if !ok {
|
||||
t.Fatalf("expected metadata for %q", FeatureKubernetesAI)
|
||||
}
|
||||
if kubernetes.DisplayableInPlanUI {
|
||||
t.Fatalf("did not expect compatibility-only %q to be displayable in plan UI", FeatureKubernetesAI)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTierMonitoredSystemLimits(t *testing.T) {
|
||||
tests := []struct {
|
||||
tier Tier
|
||||
|
|
|
|||
393
pkg/licensing/self_hosted_feature_catalog.go
Normal file
393
pkg/licensing/self_hosted_feature_catalog.go
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
package licensing
|
||||
|
||||
import "sort"
|
||||
|
||||
type SelfHostedFeatureRole string
|
||||
|
||||
const (
|
||||
SelfHostedFeatureRoleHidden SelfHostedFeatureRole = "hidden"
|
||||
SelfHostedFeatureRoleIncluded SelfHostedFeatureRole = "included"
|
||||
SelfHostedFeatureRolePrimaryPillar SelfHostedFeatureRole = "primary_pillar"
|
||||
SelfHostedFeatureRoleIncludedExtra SelfHostedFeatureRole = "included_extra"
|
||||
SelfHostedFeatureRoleCompatibilityOnly SelfHostedFeatureRole = "compatibility_only"
|
||||
)
|
||||
|
||||
type SelfHostedFeatureRoles struct {
|
||||
Community SelfHostedFeatureRole
|
||||
Relay SelfHostedFeatureRole
|
||||
Pro SelfHostedFeatureRole
|
||||
}
|
||||
|
||||
type FeatureMetadata struct {
|
||||
Key string
|
||||
DisplayName string
|
||||
ComparisonName string
|
||||
ShowInComparisonTable bool
|
||||
DisplayableInPlanUI bool
|
||||
SelfHostedRoles SelfHostedFeatureRoles
|
||||
UpgradeReason string
|
||||
UpgradePriority int
|
||||
}
|
||||
|
||||
var orderedFeatureMetadataKeys = []string{
|
||||
FeatureUpdateAlerts,
|
||||
FeatureSSO,
|
||||
FeatureAIPatrol,
|
||||
FeatureRelay,
|
||||
FeatureMobileApp,
|
||||
FeaturePushNotifications,
|
||||
FeatureAIAlerts,
|
||||
FeatureAIAutoFix,
|
||||
FeatureLongTermMetrics,
|
||||
FeatureAdvancedSSO,
|
||||
FeatureRBAC,
|
||||
FeatureAuditLogging,
|
||||
FeatureAdvancedReporting,
|
||||
FeatureAgentProfiles,
|
||||
FeatureKubernetesAI,
|
||||
FeatureMultiUser,
|
||||
FeatureWhiteLabel,
|
||||
FeatureMultiTenant,
|
||||
FeatureUnlimited,
|
||||
FeatureDemoFixtures,
|
||||
}
|
||||
|
||||
var featureMetadataCatalog = map[string]FeatureMetadata{
|
||||
FeatureUpdateAlerts: {
|
||||
Key: FeatureUpdateAlerts,
|
||||
DisplayName: "Update Alerts (Container/Package Updates)",
|
||||
ComparisonName: "Update Alerts",
|
||||
ShowInComparisonTable: true,
|
||||
DisplayableInPlanUI: true,
|
||||
SelfHostedRoles: SelfHostedFeatureRoles{
|
||||
Community: SelfHostedFeatureRoleIncluded,
|
||||
Relay: SelfHostedFeatureRoleIncluded,
|
||||
Pro: SelfHostedFeatureRoleIncluded,
|
||||
},
|
||||
},
|
||||
FeatureSSO: {
|
||||
Key: FeatureSSO,
|
||||
DisplayName: "Basic SSO (OIDC)",
|
||||
ComparisonName: "Basic SSO (OIDC)",
|
||||
ShowInComparisonTable: true,
|
||||
DisplayableInPlanUI: true,
|
||||
SelfHostedRoles: SelfHostedFeatureRoles{
|
||||
Community: SelfHostedFeatureRoleIncluded,
|
||||
Relay: SelfHostedFeatureRoleIncluded,
|
||||
Pro: SelfHostedFeatureRoleIncluded,
|
||||
},
|
||||
},
|
||||
FeatureAIPatrol: {
|
||||
Key: FeatureAIPatrol,
|
||||
DisplayName: "Pulse Patrol (Background Health Checks)",
|
||||
ComparisonName: "Pulse Patrol",
|
||||
ShowInComparisonTable: true,
|
||||
DisplayableInPlanUI: true,
|
||||
SelfHostedRoles: SelfHostedFeatureRoles{
|
||||
Community: SelfHostedFeatureRoleIncluded,
|
||||
Relay: SelfHostedFeatureRoleIncluded,
|
||||
Pro: SelfHostedFeatureRoleIncluded,
|
||||
},
|
||||
},
|
||||
FeatureRelay: {
|
||||
Key: FeatureRelay,
|
||||
DisplayName: "Pulse Relay (Remote Access)",
|
||||
ComparisonName: "Pulse Relay (Remote Access)",
|
||||
ShowInComparisonTable: true,
|
||||
DisplayableInPlanUI: true,
|
||||
SelfHostedRoles: SelfHostedFeatureRoles{
|
||||
Community: SelfHostedFeatureRoleHidden,
|
||||
Relay: SelfHostedFeatureRolePrimaryPillar,
|
||||
Pro: SelfHostedFeatureRoleIncluded,
|
||||
},
|
||||
UpgradeReason: "Get Relay so Pulse stays reachable securely from anywhere instead of only on the local dashboard.",
|
||||
UpgradePriority: 1,
|
||||
},
|
||||
FeatureMobileApp: {
|
||||
Key: FeatureMobileApp,
|
||||
DisplayName: "Mobile App Access",
|
||||
ComparisonName: "Mobile App Access",
|
||||
ShowInComparisonTable: true,
|
||||
DisplayableInPlanUI: true,
|
||||
SelfHostedRoles: SelfHostedFeatureRoles{
|
||||
Community: SelfHostedFeatureRoleHidden,
|
||||
Relay: SelfHostedFeatureRolePrimaryPillar,
|
||||
Pro: SelfHostedFeatureRoleIncluded,
|
||||
},
|
||||
UpgradeReason: "Get Relay so you can check Pulse from your phone when you are away from the dashboard.",
|
||||
UpgradePriority: 1,
|
||||
},
|
||||
FeaturePushNotifications: {
|
||||
Key: FeaturePushNotifications,
|
||||
DisplayName: "Push Notifications",
|
||||
ComparisonName: "Push Notifications",
|
||||
ShowInComparisonTable: true,
|
||||
DisplayableInPlanUI: true,
|
||||
SelfHostedRoles: SelfHostedFeatureRoles{
|
||||
Community: SelfHostedFeatureRoleHidden,
|
||||
Relay: SelfHostedFeatureRolePrimaryPillar,
|
||||
Pro: SelfHostedFeatureRoleIncluded,
|
||||
},
|
||||
UpgradeReason: "Get Relay so important alerts reach you immediately on mobile instead of waiting for you to reopen Pulse.",
|
||||
UpgradePriority: 1,
|
||||
},
|
||||
FeatureLongTermMetrics: {
|
||||
Key: FeatureLongTermMetrics,
|
||||
DisplayName: "Extended Metric History",
|
||||
ComparisonName: "Extended Metric History",
|
||||
ShowInComparisonTable: false,
|
||||
DisplayableInPlanUI: true,
|
||||
SelfHostedRoles: SelfHostedFeatureRoles{
|
||||
Community: SelfHostedFeatureRoleHidden,
|
||||
Relay: SelfHostedFeatureRolePrimaryPillar,
|
||||
Pro: SelfHostedFeatureRolePrimaryPillar,
|
||||
},
|
||||
UpgradeReason: "Get Relay for 14 days of history, or Pro for 90 days, so you can see what changed before and after an incident.",
|
||||
UpgradePriority: 2,
|
||||
},
|
||||
FeatureAIAlerts: {
|
||||
Key: FeatureAIAlerts,
|
||||
DisplayName: "Alert Analysis",
|
||||
ComparisonName: "Pulse Alert Analysis",
|
||||
ShowInComparisonTable: true,
|
||||
DisplayableInPlanUI: true,
|
||||
SelfHostedRoles: SelfHostedFeatureRoles{
|
||||
Community: SelfHostedFeatureRoleHidden,
|
||||
Relay: SelfHostedFeatureRoleHidden,
|
||||
Pro: SelfHostedFeatureRolePrimaryPillar,
|
||||
},
|
||||
UpgradeReason: "Upgrade to Pro so alerts arrive with root-cause analysis instead of a stack of symptoms.",
|
||||
UpgradePriority: 4,
|
||||
},
|
||||
FeatureAIAutoFix: {
|
||||
Key: FeatureAIAutoFix,
|
||||
DisplayName: "Pulse Patrol Auto-Fix",
|
||||
ComparisonName: "Patrol Auto-Fix",
|
||||
ShowInComparisonTable: true,
|
||||
DisplayableInPlanUI: true,
|
||||
SelfHostedRoles: SelfHostedFeatureRoles{
|
||||
Community: SelfHostedFeatureRoleHidden,
|
||||
Relay: SelfHostedFeatureRoleHidden,
|
||||
Pro: SelfHostedFeatureRolePrimaryPillar,
|
||||
},
|
||||
UpgradeReason: "Upgrade to Pro so Pulse can move from finding issues to applying safe remediation with your approval or in autonomous mode.",
|
||||
UpgradePriority: 3,
|
||||
},
|
||||
FeatureKubernetesAI: {
|
||||
Key: FeatureKubernetesAI,
|
||||
DisplayName: "Kubernetes AI Analysis (Compatibility)",
|
||||
ComparisonName: "Kubernetes AI Analysis (Compatibility)",
|
||||
ShowInComparisonTable: false,
|
||||
DisplayableInPlanUI: false,
|
||||
SelfHostedRoles: SelfHostedFeatureRoles{
|
||||
Community: SelfHostedFeatureRoleHidden,
|
||||
Relay: SelfHostedFeatureRoleHidden,
|
||||
Pro: SelfHostedFeatureRoleCompatibilityOnly,
|
||||
},
|
||||
},
|
||||
FeatureAgentProfiles: {
|
||||
Key: FeatureAgentProfiles,
|
||||
DisplayName: "Centralized Agent Profiles",
|
||||
ComparisonName: "Centralized Agent Profiles",
|
||||
ShowInComparisonTable: true,
|
||||
DisplayableInPlanUI: true,
|
||||
SelfHostedRoles: SelfHostedFeatureRoles{
|
||||
Community: SelfHostedFeatureRoleHidden,
|
||||
Relay: SelfHostedFeatureRoleHidden,
|
||||
Pro: SelfHostedFeatureRoleIncludedExtra,
|
||||
},
|
||||
UpgradeReason: "Upgrade to Pro to standardize agent behavior across systems without reconfiguring every install by hand.",
|
||||
UpgradePriority: 6,
|
||||
},
|
||||
FeatureRBAC: {
|
||||
Key: FeatureRBAC,
|
||||
DisplayName: "Role-Based Access Control (RBAC)",
|
||||
ComparisonName: "Role-Based Access Control (RBAC)",
|
||||
ShowInComparisonTable: true,
|
||||
DisplayableInPlanUI: true,
|
||||
SelfHostedRoles: SelfHostedFeatureRoles{
|
||||
Community: SelfHostedFeatureRoleHidden,
|
||||
Relay: SelfHostedFeatureRoleHidden,
|
||||
Pro: SelfHostedFeatureRoleIncludedExtra,
|
||||
},
|
||||
UpgradeReason: "Upgrade to Pro when more than one operator needs safe access boundaries around infrastructure changes.",
|
||||
UpgradePriority: 5,
|
||||
},
|
||||
FeatureAuditLogging: {
|
||||
Key: FeatureAuditLogging,
|
||||
DisplayName: "Audit Logging",
|
||||
ComparisonName: "Audit Logging",
|
||||
ShowInComparisonTable: true,
|
||||
DisplayableInPlanUI: true,
|
||||
SelfHostedRoles: SelfHostedFeatureRoles{
|
||||
Community: SelfHostedFeatureRoleHidden,
|
||||
Relay: SelfHostedFeatureRoleHidden,
|
||||
Pro: SelfHostedFeatureRoleIncludedExtra,
|
||||
},
|
||||
UpgradeReason: "Upgrade to Pro to keep a trustworthy action trail for incident review, accountability, and compliance.",
|
||||
UpgradePriority: 8,
|
||||
},
|
||||
FeatureAdvancedSSO: {
|
||||
Key: FeatureAdvancedSSO,
|
||||
DisplayName: "Advanced SSO (SAML/Multi-Provider)",
|
||||
ComparisonName: "Advanced SSO (SAML/Multi-Provider)",
|
||||
ShowInComparisonTable: true,
|
||||
DisplayableInPlanUI: true,
|
||||
SelfHostedRoles: SelfHostedFeatureRoles{
|
||||
Community: SelfHostedFeatureRoleHidden,
|
||||
Relay: SelfHostedFeatureRoleHidden,
|
||||
Pro: SelfHostedFeatureRoleIncludedExtra,
|
||||
},
|
||||
UpgradeReason: "Upgrade to Pro to connect your identity provider and keep operator access aligned with your existing auth controls.",
|
||||
UpgradePriority: 7,
|
||||
},
|
||||
FeatureAdvancedReporting: {
|
||||
Key: FeatureAdvancedReporting,
|
||||
DisplayName: "PDF/CSV Reporting",
|
||||
ComparisonName: "PDF/CSV Reporting",
|
||||
ShowInComparisonTable: true,
|
||||
DisplayableInPlanUI: true,
|
||||
SelfHostedRoles: SelfHostedFeatureRoles{
|
||||
Community: SelfHostedFeatureRoleHidden,
|
||||
Relay: SelfHostedFeatureRoleHidden,
|
||||
Pro: SelfHostedFeatureRoleIncludedExtra,
|
||||
},
|
||||
UpgradeReason: "Upgrade to Pro to turn live infrastructure state into shareable reports without manual screenshot work.",
|
||||
UpgradePriority: 9,
|
||||
},
|
||||
FeatureMultiUser: {
|
||||
Key: FeatureMultiUser,
|
||||
DisplayName: "Multi-User Mode",
|
||||
ComparisonName: "Multi-User Mode",
|
||||
DisplayableInPlanUI: false,
|
||||
SelfHostedRoles: SelfHostedFeatureRoles{
|
||||
Community: SelfHostedFeatureRoleHidden,
|
||||
Relay: SelfHostedFeatureRoleHidden,
|
||||
Pro: SelfHostedFeatureRoleHidden,
|
||||
},
|
||||
},
|
||||
FeatureWhiteLabel: {
|
||||
Key: FeatureWhiteLabel,
|
||||
DisplayName: "White-Label Branding",
|
||||
ComparisonName: "White-Label Branding",
|
||||
DisplayableInPlanUI: false,
|
||||
SelfHostedRoles: SelfHostedFeatureRoles{
|
||||
Community: SelfHostedFeatureRoleHidden,
|
||||
Relay: SelfHostedFeatureRoleHidden,
|
||||
Pro: SelfHostedFeatureRoleHidden,
|
||||
},
|
||||
},
|
||||
FeatureMultiTenant: {
|
||||
Key: FeatureMultiTenant,
|
||||
DisplayName: "Multi-Tenant Mode",
|
||||
ComparisonName: "Multi-Tenant Mode",
|
||||
DisplayableInPlanUI: true,
|
||||
SelfHostedRoles: SelfHostedFeatureRoles{
|
||||
Community: SelfHostedFeatureRoleHidden,
|
||||
Relay: SelfHostedFeatureRoleHidden,
|
||||
Pro: SelfHostedFeatureRoleHidden,
|
||||
},
|
||||
},
|
||||
FeatureUnlimited: {
|
||||
Key: FeatureUnlimited,
|
||||
DisplayName: "Unlimited Instances",
|
||||
ComparisonName: "Unlimited Instances",
|
||||
DisplayableInPlanUI: false,
|
||||
SelfHostedRoles: SelfHostedFeatureRoles{
|
||||
Community: SelfHostedFeatureRoleHidden,
|
||||
Relay: SelfHostedFeatureRoleHidden,
|
||||
Pro: SelfHostedFeatureRoleHidden,
|
||||
},
|
||||
},
|
||||
FeatureDemoFixtures: {
|
||||
Key: FeatureDemoFixtures,
|
||||
DisplayName: "Demo Fixtures (Internal)",
|
||||
ComparisonName: "Demo Fixtures (Internal)",
|
||||
DisplayableInPlanUI: false,
|
||||
SelfHostedRoles: SelfHostedFeatureRoles{
|
||||
Community: SelfHostedFeatureRoleHidden,
|
||||
Relay: SelfHostedFeatureRoleHidden,
|
||||
Pro: SelfHostedFeatureRoleHidden,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func AllFeatureMetadata() []FeatureMetadata {
|
||||
metadata := make([]FeatureMetadata, 0, len(orderedFeatureMetadataKeys))
|
||||
for _, key := range orderedFeatureMetadataKeys {
|
||||
entry, ok := featureMetadataCatalog[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
metadata = append(metadata, entry)
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
|
||||
func GetFeatureMetadata(feature string) (FeatureMetadata, bool) {
|
||||
entry, ok := featureMetadataCatalog[feature]
|
||||
return entry, ok
|
||||
}
|
||||
|
||||
func GetSelfHostedFeatureRole(feature string, tier Tier) SelfHostedFeatureRole {
|
||||
entry, ok := featureMetadataCatalog[feature]
|
||||
if !ok {
|
||||
return SelfHostedFeatureRoleHidden
|
||||
}
|
||||
switch tier {
|
||||
case TierFree:
|
||||
return entry.SelfHostedRoles.Community
|
||||
case TierRelay:
|
||||
return entry.SelfHostedRoles.Relay
|
||||
case TierPro, TierProPlus, TierProAnnual, TierLifetime:
|
||||
return entry.SelfHostedRoles.Pro
|
||||
default:
|
||||
return SelfHostedFeatureRoleHidden
|
||||
}
|
||||
}
|
||||
|
||||
func IsCompatibilityOnlyFeature(feature string) bool {
|
||||
entry, ok := featureMetadataCatalog[feature]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return entry.SelfHostedRoles.Community == SelfHostedFeatureRoleCompatibilityOnly ||
|
||||
entry.SelfHostedRoles.Relay == SelfHostedFeatureRoleCompatibilityOnly ||
|
||||
entry.SelfHostedRoles.Pro == SelfHostedFeatureRoleCompatibilityOnly
|
||||
}
|
||||
|
||||
func SelfHostedComparisonFeatures() []FeatureMetadata {
|
||||
out := make([]FeatureMetadata, 0)
|
||||
for _, entry := range AllFeatureMetadata() {
|
||||
if entry.ShowInComparisonTable {
|
||||
out = append(out, entry)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func SelfHostedPlanFeaturesForRole(tier Tier, role SelfHostedFeatureRole) []FeatureMetadata {
|
||||
out := make([]FeatureMetadata, 0)
|
||||
for _, entry := range AllFeatureMetadata() {
|
||||
if GetSelfHostedFeatureRole(entry.Key, tier) == role {
|
||||
out = append(out, entry)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func GenericUpgradeFeatureMetadata() []FeatureMetadata {
|
||||
out := make([]FeatureMetadata, 0)
|
||||
for _, entry := range AllFeatureMetadata() {
|
||||
if entry.UpgradeReason != "" {
|
||||
out = append(out, entry)
|
||||
}
|
||||
}
|
||||
sort.SliceStable(out, func(i, j int) bool {
|
||||
if out[i].UpgradePriority == out[j].UpgradePriority {
|
||||
return out[i].Key < out[j].Key
|
||||
}
|
||||
return out[i].UpgradePriority < out[j].UpgradePriority
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
|
@ -14,73 +14,20 @@ type ReasonEntry struct {
|
|||
// Relay features use "Upgrade to Relay" messaging; Pro features use "Upgrade to Pro".
|
||||
// Compatibility-only capabilities must not appear here as generic marketed
|
||||
// upgrade reasons.
|
||||
var UpgradeReasonMatrix = []ReasonEntry{
|
||||
{
|
||||
Feature: FeatureRelay,
|
||||
Reason: "Get Relay so Pulse stays reachable securely from anywhere instead of only on the local dashboard.",
|
||||
ActionURL: UpgradeURLForFeature(FeatureRelay),
|
||||
Priority: 1,
|
||||
},
|
||||
{
|
||||
Feature: FeatureMobileApp,
|
||||
Reason: "Get Relay so you can check Pulse from your phone when you are away from the dashboard.",
|
||||
ActionURL: UpgradeURLForFeature(FeatureMobileApp),
|
||||
Priority: 1,
|
||||
},
|
||||
{
|
||||
Feature: FeaturePushNotifications,
|
||||
Reason: "Get Relay so important alerts reach you immediately on mobile instead of waiting for you to reopen Pulse.",
|
||||
ActionURL: UpgradeURLForFeature(FeaturePushNotifications),
|
||||
Priority: 1,
|
||||
},
|
||||
{
|
||||
Feature: FeatureLongTermMetrics,
|
||||
Reason: "Get Relay for 14 days of history, or Pro for 90 days, so you can see what changed before and after an incident.",
|
||||
ActionURL: UpgradeURLForFeature(FeatureLongTermMetrics),
|
||||
Priority: 2,
|
||||
},
|
||||
{
|
||||
Feature: FeatureAIAutoFix,
|
||||
Reason: "Upgrade to Pro so Pulse can move from finding issues to applying safe remediation with your approval or in autonomous mode.",
|
||||
ActionURL: UpgradeURLForFeature(FeatureAIAutoFix),
|
||||
Priority: 3,
|
||||
},
|
||||
{
|
||||
Feature: FeatureAIAlerts,
|
||||
Reason: "Upgrade to Pro so alerts arrive with root-cause analysis instead of a stack of symptoms.",
|
||||
ActionURL: UpgradeURLForFeature(FeatureAIAlerts),
|
||||
Priority: 4,
|
||||
},
|
||||
{
|
||||
Feature: FeatureRBAC,
|
||||
Reason: "Upgrade to Pro when more than one operator needs safe access boundaries around infrastructure changes.",
|
||||
ActionURL: UpgradeURLForFeature(FeatureRBAC),
|
||||
Priority: 5,
|
||||
},
|
||||
{
|
||||
Feature: FeatureAgentProfiles,
|
||||
Reason: "Upgrade to Pro to standardize agent behavior across systems without reconfiguring every install by hand.",
|
||||
ActionURL: UpgradeURLForFeature(FeatureAgentProfiles),
|
||||
Priority: 6,
|
||||
},
|
||||
{
|
||||
Feature: FeatureAdvancedSSO,
|
||||
Reason: "Upgrade to Pro to connect your identity provider and keep operator access aligned with your existing auth controls.",
|
||||
ActionURL: UpgradeURLForFeature(FeatureAdvancedSSO),
|
||||
Priority: 7,
|
||||
},
|
||||
{
|
||||
Feature: FeatureAuditLogging,
|
||||
Reason: "Upgrade to Pro to keep a trustworthy action trail for incident review, accountability, and compliance.",
|
||||
ActionURL: UpgradeURLForFeature(FeatureAuditLogging),
|
||||
Priority: 8,
|
||||
},
|
||||
{
|
||||
Feature: FeatureAdvancedReporting,
|
||||
Reason: "Upgrade to Pro to turn live infrastructure state into shareable reports without manual screenshot work.",
|
||||
ActionURL: UpgradeURLForFeature(FeatureAdvancedReporting),
|
||||
Priority: 9,
|
||||
},
|
||||
var UpgradeReasonMatrix = buildUpgradeReasonMatrix()
|
||||
|
||||
func buildUpgradeReasonMatrix() []ReasonEntry {
|
||||
metadata := GenericUpgradeFeatureMetadata()
|
||||
reasons := make([]ReasonEntry, 0, len(metadata))
|
||||
for _, entry := range metadata {
|
||||
reasons = append(reasons, ReasonEntry{
|
||||
Feature: entry.Key,
|
||||
Reason: entry.UpgradeReason,
|
||||
ActionURL: UpgradeURLForFeature(entry.Key),
|
||||
Priority: entry.UpgradePriority,
|
||||
})
|
||||
}
|
||||
return reasons
|
||||
}
|
||||
|
||||
// GenerateUpgradeReasons returns upgrade reasons for features missing in capabilities.
|
||||
|
|
|
|||
|
|
@ -109,3 +109,29 @@ func TestCompatibilityOnlyFeaturesStayOutOfGenericUpgradeReasons(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpgradeReasonMatrixDerivesFromCanonicalFeatureMetadata(t *testing.T) {
|
||||
expectedOrder := []string{
|
||||
FeatureMobileApp,
|
||||
FeaturePushNotifications,
|
||||
FeatureRelay,
|
||||
FeatureLongTermMetrics,
|
||||
FeatureAIAutoFix,
|
||||
FeatureAIAlerts,
|
||||
FeatureRBAC,
|
||||
FeatureAgentProfiles,
|
||||
FeatureAdvancedSSO,
|
||||
FeatureAuditLogging,
|
||||
FeatureAdvancedReporting,
|
||||
}
|
||||
|
||||
if len(UpgradeReasonMatrix) != len(expectedOrder) {
|
||||
t.Fatalf("UpgradeReasonMatrix length = %d, want %d", len(UpgradeReasonMatrix), len(expectedOrder))
|
||||
}
|
||||
|
||||
for idx, feature := range expectedOrder {
|
||||
if UpgradeReasonMatrix[idx].Feature != feature {
|
||||
t.Fatalf("UpgradeReasonMatrix[%d].Feature = %q, want %q", idx, UpgradeReasonMatrix[idx].Feature, feature)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
269
scripts/generate-self-hosted-feature-catalog.go
Normal file
269
scripts/generate-self-hosted-feature-catalog.go
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/pkg/licensing"
|
||||
)
|
||||
|
||||
type generatedFeatureEntry struct {
|
||||
Key string `json:"key"`
|
||||
DisplayName string `json:"displayName"`
|
||||
ComparisonName string `json:"comparisonName"`
|
||||
ShowInComparisonTable bool `json:"showInComparisonTable"`
|
||||
DisplayableInSelfHostedPlan bool `json:"displayableInSelfHostedPlan"`
|
||||
Roles generatedFeatureRoles `json:"roles"`
|
||||
IncludedIn generatedFeatureTierInclusion `json:"includedIn"`
|
||||
}
|
||||
|
||||
type generatedFeatureRoles struct {
|
||||
Community licensing.SelfHostedFeatureRole `json:"community"`
|
||||
Relay licensing.SelfHostedFeatureRole `json:"relay"`
|
||||
Pro licensing.SelfHostedFeatureRole `json:"pro"`
|
||||
}
|
||||
|
||||
type generatedFeatureTierInclusion struct {
|
||||
Community bool `json:"community"`
|
||||
Relay bool `json:"relay"`
|
||||
Pro bool `json:"pro"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
repoRoot, err := os.Getwd()
|
||||
if err != nil {
|
||||
fail(err)
|
||||
}
|
||||
|
||||
entries := buildGeneratedEntries()
|
||||
tierFeatures := map[string][]string{
|
||||
"community": append([]string(nil), licensing.TierFeatures[licensing.TierFree]...),
|
||||
"relay": append([]string(nil), licensing.TierFeatures[licensing.TierRelay]...),
|
||||
"pro": append([]string(nil), licensing.TierFeatures[licensing.TierPro]...),
|
||||
}
|
||||
|
||||
if err := writeFrontendModule(filepath.Join(repoRoot, "frontend-modern/src/utils/selfHostedFeatureCatalog.generated.ts"), entries); err != nil {
|
||||
fail(err)
|
||||
}
|
||||
|
||||
pulseProRoot := filepath.Clean(filepath.Join(repoRoot, "..", "pulse-pro"))
|
||||
if _, err := os.Stat(pulseProRoot); err == nil {
|
||||
if err := writePulseProModule(filepath.Join(pulseProRoot, "license-server/self_hosted_feature_catalog.generated.go"), entries, tierFeatures); err != nil {
|
||||
fail(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildGeneratedEntries() []generatedFeatureEntry {
|
||||
metadata := licensing.AllFeatureMetadata()
|
||||
entries := make([]generatedFeatureEntry, 0, len(metadata))
|
||||
for _, entry := range metadata {
|
||||
roles := generatedFeatureRoles{
|
||||
Community: entry.SelfHostedRoles.Community,
|
||||
Relay: entry.SelfHostedRoles.Relay,
|
||||
Pro: entry.SelfHostedRoles.Pro,
|
||||
}
|
||||
included := generatedFeatureTierInclusion{
|
||||
Community: licensing.TierHasFeature(licensing.TierFree, entry.Key),
|
||||
Relay: licensing.TierHasFeature(licensing.TierRelay, entry.Key),
|
||||
Pro: licensing.TierHasFeature(licensing.TierPro, entry.Key),
|
||||
}
|
||||
entries = append(entries, generatedFeatureEntry{
|
||||
Key: entry.Key,
|
||||
DisplayName: entry.DisplayName,
|
||||
ComparisonName: entry.ComparisonName,
|
||||
ShowInComparisonTable: entry.ShowInComparisonTable,
|
||||
DisplayableInSelfHostedPlan: entry.DisplayableInPlanUI,
|
||||
Roles: roles,
|
||||
IncludedIn: included,
|
||||
})
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
func writeFrontendModule(path string, entries []generatedFeatureEntry) error {
|
||||
payload, err := json.MarshalIndent(entries, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("// This file is generated by scripts/generate-self-hosted-feature-catalog.go.\n")
|
||||
buf.WriteString("// Do not edit it manually.\n\n")
|
||||
buf.WriteString("export type SelfHostedFeatureRole =\n")
|
||||
buf.WriteString(" | 'hidden'\n")
|
||||
buf.WriteString(" | 'included'\n")
|
||||
buf.WriteString(" | 'primary_pillar'\n")
|
||||
buf.WriteString(" | 'included_extra'\n")
|
||||
buf.WriteString(" | 'compatibility_only';\n\n")
|
||||
buf.WriteString("export interface GeneratedSelfHostedFeatureCatalogEntry {\n")
|
||||
buf.WriteString(" key: string;\n")
|
||||
buf.WriteString(" displayName: string;\n")
|
||||
buf.WriteString(" comparisonName: string;\n")
|
||||
buf.WriteString(" showInComparisonTable: boolean;\n")
|
||||
buf.WriteString(" displayableInSelfHostedPlan: boolean;\n")
|
||||
buf.WriteString(" roles: {\n")
|
||||
buf.WriteString(" community: SelfHostedFeatureRole;\n")
|
||||
buf.WriteString(" relay: SelfHostedFeatureRole;\n")
|
||||
buf.WriteString(" pro: SelfHostedFeatureRole;\n")
|
||||
buf.WriteString(" };\n")
|
||||
buf.WriteString(" includedIn: {\n")
|
||||
buf.WriteString(" community: boolean;\n")
|
||||
buf.WriteString(" relay: boolean;\n")
|
||||
buf.WriteString(" pro: boolean;\n")
|
||||
buf.WriteString(" };\n")
|
||||
buf.WriteString("}\n\n")
|
||||
buf.WriteString("export const SELF_HOSTED_FEATURE_CATALOG: readonly GeneratedSelfHostedFeatureCatalogEntry[] = ")
|
||||
buf.Write(payload)
|
||||
buf.WriteString(" as const;\n\n")
|
||||
buf.WriteString("export const SELF_HOSTED_FEATURE_CATALOG_BY_KEY: Record<string, GeneratedSelfHostedFeatureCatalogEntry> = Object.fromEntries(\n")
|
||||
buf.WriteString(" SELF_HOSTED_FEATURE_CATALOG.map((entry) => [entry.key, entry]),\n")
|
||||
buf.WriteString(") as Record<string, GeneratedSelfHostedFeatureCatalogEntry>;\n\n")
|
||||
buf.WriteString("export function getSelfHostedFeatureCatalogEntry(\n")
|
||||
buf.WriteString(" key?: string | null,\n")
|
||||
buf.WriteString("): GeneratedSelfHostedFeatureCatalogEntry | null {\n")
|
||||
buf.WriteString(" const normalized = (key || '').trim();\n")
|
||||
buf.WriteString(" if (!normalized) {\n")
|
||||
buf.WriteString(" return null;\n")
|
||||
buf.WriteString(" }\n")
|
||||
buf.WriteString(" return SELF_HOSTED_FEATURE_CATALOG_BY_KEY[normalized] || null;\n")
|
||||
buf.WriteString("}\n\n")
|
||||
buf.WriteString("export function isDisplayableSelfHostedFeatureKey(key?: string | null): boolean {\n")
|
||||
buf.WriteString(" const entry = getSelfHostedFeatureCatalogEntry(key);\n")
|
||||
buf.WriteString(" if (!entry) {\n")
|
||||
buf.WriteString(" return true;\n")
|
||||
buf.WriteString(" }\n")
|
||||
buf.WriteString(" return entry.displayableInSelfHostedPlan;\n")
|
||||
buf.WriteString("}\n\n")
|
||||
buf.WriteString("export function getSelfHostedFeaturesForRole(\n")
|
||||
buf.WriteString(" tier: 'community' | 'relay' | 'pro',\n")
|
||||
buf.WriteString(" role: SelfHostedFeatureRole,\n")
|
||||
buf.WriteString("): readonly GeneratedSelfHostedFeatureCatalogEntry[] {\n")
|
||||
buf.WriteString(" return SELF_HOSTED_FEATURE_CATALOG.filter((entry) => entry.roles[tier] === role);\n")
|
||||
buf.WriteString("}\n")
|
||||
|
||||
return os.WriteFile(path, buf.Bytes(), 0o644)
|
||||
}
|
||||
|
||||
func writePulseProModule(path string, entries []generatedFeatureEntry, tierFeatures map[string][]string) error {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("// Code generated by ../pulse/scripts/generate-self-hosted-feature-catalog.go; DO NOT EDIT.\n")
|
||||
buf.WriteString("package main\n\n")
|
||||
buf.WriteString("import \"strings\"\n\n")
|
||||
buf.WriteString("type selfHostedFeatureRole string\n\n")
|
||||
buf.WriteString("const (\n")
|
||||
buf.WriteString("\tselfHostedFeatureRoleHidden selfHostedFeatureRole = \"hidden\"\n")
|
||||
buf.WriteString("\tselfHostedFeatureRoleIncluded selfHostedFeatureRole = \"included\"\n")
|
||||
buf.WriteString("\tselfHostedFeatureRolePrimaryPillar selfHostedFeatureRole = \"primary_pillar\"\n")
|
||||
buf.WriteString("\tselfHostedFeatureRoleIncludedExtra selfHostedFeatureRole = \"included_extra\"\n")
|
||||
buf.WriteString("\tselfHostedFeatureRoleCompatibilityOnly selfHostedFeatureRole = \"compatibility_only\"\n")
|
||||
buf.WriteString(")\n\n")
|
||||
buf.WriteString("type selfHostedFeatureCatalogEntry struct {\n")
|
||||
buf.WriteString("\tKey string\n")
|
||||
buf.WriteString("\tDisplayName string\n")
|
||||
buf.WriteString("\tComparisonName string\n")
|
||||
buf.WriteString("\tShowInComparisonTable bool\n")
|
||||
buf.WriteString("\tDisplayableInSelfHostedPlan bool\n")
|
||||
buf.WriteString("\tCommunityRole selfHostedFeatureRole\n")
|
||||
buf.WriteString("\tRelayRole selfHostedFeatureRole\n")
|
||||
buf.WriteString("\tProRole selfHostedFeatureRole\n")
|
||||
buf.WriteString("\tCommunity bool\n")
|
||||
buf.WriteString("\tRelay bool\n")
|
||||
buf.WriteString("\tPro bool\n")
|
||||
buf.WriteString("}\n\n")
|
||||
|
||||
buf.WriteString("var selfHostedFeatureCatalog = []selfHostedFeatureCatalogEntry{\n")
|
||||
for _, entry := range entries {
|
||||
buf.WriteString("\t{\n")
|
||||
buf.WriteString(fmt.Sprintf("\t\tKey: %q,\n", entry.Key))
|
||||
buf.WriteString(fmt.Sprintf("\t\tDisplayName: %q,\n", entry.DisplayName))
|
||||
buf.WriteString(fmt.Sprintf("\t\tComparisonName: %q,\n", entry.ComparisonName))
|
||||
buf.WriteString(fmt.Sprintf("\t\tShowInComparisonTable: %t,\n", entry.ShowInComparisonTable))
|
||||
buf.WriteString(fmt.Sprintf("\t\tDisplayableInSelfHostedPlan: %t,\n", entry.DisplayableInSelfHostedPlan))
|
||||
buf.WriteString(fmt.Sprintf("\t\tCommunityRole: %q,\n", entry.Roles.Community))
|
||||
buf.WriteString(fmt.Sprintf("\t\tRelayRole: %q,\n", entry.Roles.Relay))
|
||||
buf.WriteString(fmt.Sprintf("\t\tProRole: %q,\n", entry.Roles.Pro))
|
||||
buf.WriteString(fmt.Sprintf("\t\tCommunity: %t,\n", entry.IncludedIn.Community))
|
||||
buf.WriteString(fmt.Sprintf("\t\tRelay: %t,\n", entry.IncludedIn.Relay))
|
||||
buf.WriteString(fmt.Sprintf("\t\tPro: %t,\n", entry.IncludedIn.Pro))
|
||||
buf.WriteString("\t},\n")
|
||||
}
|
||||
buf.WriteString("}\n\n")
|
||||
|
||||
buf.WriteString("var selfHostedFeatureCatalogByKey = map[string]selfHostedFeatureCatalogEntry{\n")
|
||||
for _, entry := range entries {
|
||||
buf.WriteString(fmt.Sprintf("\t%q: {\n", entry.Key))
|
||||
buf.WriteString(fmt.Sprintf("\t\tKey: %q,\n", entry.Key))
|
||||
buf.WriteString(fmt.Sprintf("\t\tDisplayName: %q,\n", entry.DisplayName))
|
||||
buf.WriteString(fmt.Sprintf("\t\tComparisonName: %q,\n", entry.ComparisonName))
|
||||
buf.WriteString(fmt.Sprintf("\t\tShowInComparisonTable: %t,\n", entry.ShowInComparisonTable))
|
||||
buf.WriteString(fmt.Sprintf("\t\tDisplayableInSelfHostedPlan: %t,\n", entry.DisplayableInSelfHostedPlan))
|
||||
buf.WriteString(fmt.Sprintf("\t\tCommunityRole: %q,\n", entry.Roles.Community))
|
||||
buf.WriteString(fmt.Sprintf("\t\tRelayRole: %q,\n", entry.Roles.Relay))
|
||||
buf.WriteString(fmt.Sprintf("\t\tProRole: %q,\n", entry.Roles.Pro))
|
||||
buf.WriteString(fmt.Sprintf("\t\tCommunity: %t,\n", entry.IncludedIn.Community))
|
||||
buf.WriteString(fmt.Sprintf("\t\tRelay: %t,\n", entry.IncludedIn.Relay))
|
||||
buf.WriteString(fmt.Sprintf("\t\tPro: %t,\n", entry.IncludedIn.Pro))
|
||||
buf.WriteString("\t},\n")
|
||||
}
|
||||
buf.WriteString("}\n\n")
|
||||
|
||||
buf.WriteString("var canonicalSelfHostedTierFeatures = map[string][]string{\n")
|
||||
tiers := make([]string, 0, len(tierFeatures))
|
||||
for tier := range tierFeatures {
|
||||
tiers = append(tiers, tier)
|
||||
}
|
||||
sort.Strings(tiers)
|
||||
for _, tier := range tiers {
|
||||
features := append([]string(nil), tierFeatures[tier]...)
|
||||
sort.Strings(features)
|
||||
buf.WriteString(fmt.Sprintf("\t%q: []string{%s},\n", tier, quoteJoin(features)))
|
||||
}
|
||||
buf.WriteString("}\n\n")
|
||||
|
||||
buf.WriteString("func selfHostedFeaturesForRole(tier string, role selfHostedFeatureRole) []selfHostedFeatureCatalogEntry {\n")
|
||||
buf.WriteString("\tentries := make([]selfHostedFeatureCatalogEntry, 0)\n")
|
||||
buf.WriteString("\tfor _, entry := range selfHostedFeatureCatalog {\n")
|
||||
buf.WriteString("\t\tvar current selfHostedFeatureRole\n")
|
||||
buf.WriteString("\t\tswitch strings.ToLower(strings.TrimSpace(tier)) {\n")
|
||||
buf.WriteString("\t\tcase \"community\":\n")
|
||||
buf.WriteString("\t\t\tcurrent = entry.CommunityRole\n")
|
||||
buf.WriteString("\t\tcase \"relay\":\n")
|
||||
buf.WriteString("\t\t\tcurrent = entry.RelayRole\n")
|
||||
buf.WriteString("\t\tcase \"pro\":\n")
|
||||
buf.WriteString("\t\t\tcurrent = entry.ProRole\n")
|
||||
buf.WriteString("\t\tdefault:\n")
|
||||
buf.WriteString("\t\t\tcurrent = selfHostedFeatureRoleHidden\n")
|
||||
buf.WriteString("\t\t}\n")
|
||||
buf.WriteString("\t\tif current == role {\n")
|
||||
buf.WriteString("\t\t\tentries = append(entries, entry)\n")
|
||||
buf.WriteString("\t\t}\n")
|
||||
buf.WriteString("\t}\n")
|
||||
buf.WriteString("\treturn entries\n")
|
||||
buf.WriteString("}\n")
|
||||
|
||||
formatted, err := format.Source(buf.Bytes())
|
||||
if err != nil {
|
||||
return fmt.Errorf("format pulse-pro module: %w", err)
|
||||
}
|
||||
return os.WriteFile(path, formatted, 0o644)
|
||||
}
|
||||
|
||||
func quoteJoin(values []string) string {
|
||||
quoted := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
quoted = append(quoted, fmt.Sprintf("%q", value))
|
||||
}
|
||||
return strings.Join(quoted, ", ")
|
||||
}
|
||||
|
||||
func fail(err error) {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue