Website: Update pricing features yaml and pricing page. (#15294)
Closes: #15265 Changes: - Updated `pricing-features-table.yml`: - Changed the structure of the file so it is a flat array of features (previously, features were nested under a category) - Added `productCategories` arrays to features that did not have them - Added `usualDepartment` values to features. - Updated the pricing features validation in `build-static-content` to work with the new file structure and made `productCategories` a required value for features. - Updated `view-pricing.js` to: - categorize features based on the values of the `productCategories` array - build a single array of features (previously, it would also build an array of features for security-focused buyers). - sort premium features to the bottom of the pricing table. - Updated the `pricing.ejs` to: - render only the list of all features server-side - conditionally show features in the pricing table, depending on the selected pricing mode
This commit is contained in:
File diff suppressed because it is too large
Load Diff
+20
-30
@@ -25,41 +25,31 @@ module.exports = {
|
||||
if(!_.isObject(sails.config.builtStaticContent) || !_.isArray(sails.config.builtStaticContent.pricingTable)) {
|
||||
throw {badConfig: 'builtStaticContent.pricingTable'};
|
||||
}
|
||||
let pricingTable = sails.config.builtStaticContent.pricingTable;
|
||||
let pricingTableFeatures = sails.config.builtStaticContent.pricingTable;
|
||||
|
||||
// Create a filtered version of the pricing table array that does not have the "Device management" category that will be used for the security-focused pricing table.
|
||||
let pricingTableForSecurity = pricingTable.filter((category)=>{
|
||||
return category.categoryName !== 'Device management';
|
||||
});
|
||||
let pricingTable = [];
|
||||
|
||||
// Create an array used to sort the pricing table for secuirty focused buyers
|
||||
// To change the order of the pricing table for the security focused buyers, rearrange the values in the array below.
|
||||
// Note: The category names must match existing categories in the pricing-features-table.yml file.
|
||||
let categoryOrderForSecurityPricingTable = [
|
||||
'Security and compliance',
|
||||
'Monitoring',
|
||||
'Inventory management',
|
||||
'Collaboration',
|
||||
'Support',
|
||||
'Data outputs',
|
||||
'Deployment'
|
||||
];
|
||||
|
||||
// Sort the security-focused pricing table from the order of the elements in the categoryOrderForSecurityPricingTable array.
|
||||
pricingTableForSecurity.sort((a, b)=>{
|
||||
// If there is a category that is not in the list above, sort it to the end of the list.
|
||||
if(categoryOrderForSecurityPricingTable.indexOf(a.categoryName) === -1){
|
||||
return 1;
|
||||
} else if(categoryOrderForSecurityPricingTable.indexOf(b.categoryName) === -1) {
|
||||
return -1;
|
||||
}
|
||||
return categoryOrderForSecurityPricingTable.indexOf(a.categoryName) - categoryOrderForSecurityPricingTable.indexOf(b.categoryName);
|
||||
});
|
||||
// Note: These product categories are hardcoded in to reduce complexity, an alternative way of building this from the pricingFeaturesTable is: let productCategories = _.union(_.flatten(_.pluck(pricingTableFeatures, 'productCategories')));
|
||||
let productCategories = ['Endpoint operations', 'Device management', 'Vulnerability management'];
|
||||
for(let category of productCategories) {
|
||||
// Get all the features in that have a productCategories array that contains this category.
|
||||
let featuresInThisCategory = _.filter(pricingTableFeatures, (feature)=>{
|
||||
return _.contains(feature.productCategories, category);
|
||||
});
|
||||
// Build a dictionary containing the category name, and all features in the category, sorting premium features to the bottom of the list.
|
||||
let allFeaturesInThisCategory = {
|
||||
categoryName: category,
|
||||
features: _.sortBy(featuresInThisCategory, (feature)=>{
|
||||
return feature.tier !== 'Free';
|
||||
})
|
||||
};
|
||||
// Add the dictionaries to the arrays that we'll use to build the features table.
|
||||
pricingTable.push(allFeaturesInThisCategory);
|
||||
}
|
||||
|
||||
// Respond with view.
|
||||
return {
|
||||
pricingTable,
|
||||
pricingTableForSecurity,
|
||||
pricingTable
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
+4
-4
@@ -62,11 +62,11 @@
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.security-selected {
|
||||
.Security-selected {
|
||||
width: 251px;
|
||||
transform: translateX(167px);
|
||||
}
|
||||
.it-selected {
|
||||
.IT-selected {
|
||||
width: 183px;
|
||||
transform: translateX(416px);
|
||||
}
|
||||
@@ -447,11 +447,11 @@
|
||||
[purpose='pricing-switch-option'] {
|
||||
height: 56px;
|
||||
}
|
||||
.security-selected {
|
||||
.Security-selected {
|
||||
width: 100%;
|
||||
transform: translateY(56px);
|
||||
}
|
||||
.it-selected {
|
||||
.IT-selected {
|
||||
width: 100%;
|
||||
transform: translateY(113px);
|
||||
}
|
||||
|
||||
+35
-34
@@ -786,46 +786,47 @@ module.exports = {
|
||||
// Validate the pricing table yaml and add it to builtStaticContent.pricingTable.
|
||||
let RELATIVE_PATH_TO_PRICING_TABLE_YML_IN_FLEET_REPO = 'handbook/company/pricing-features-table.yml';
|
||||
let yaml = await sails.helpers.fs.read(path.join(topLvlRepoPath, RELATIVE_PATH_TO_PRICING_TABLE_YML_IN_FLEET_REPO)).intercept('doesNotExist', (err)=>new Error(`Could not find pricing table features YAML file at "${RELATIVE_PATH_TO_PRICING_TABLE_YML_IN_FLEET_REPO}". Was it accidentally moved? Raw error: `+err.message));
|
||||
let pricingTableCategories = YAML.parse(yaml, {prettyErrors: true});
|
||||
|
||||
for(let category of pricingTableCategories){
|
||||
if(!category.categoryName){ // Throw an error if a category is missing a categoryName.
|
||||
throw new Error('Could not build pricing table config from pricing-features-table.yml, a category in the pricing table configuration is missing a categoryName. To resolve, make sure every category in the pricing table YAML file has a categoryName');
|
||||
let pricingTableFeatures = YAML.parse(yaml, {prettyErrors: true});
|
||||
let VALID_PRODUCT_CATEGORIES = ['Endpoint operations', 'Device management', 'Vulnerability management'];
|
||||
for(let feature of pricingTableFeatures){
|
||||
if(feature.name) {// Compatibility check
|
||||
throw new Error(`Could not build pricing table config from pricing-features-table.yml. A feature has a "name" (${feature.name}) which is no longer supported. To resolve, add a "industryName" to this feature: ${feature}`);
|
||||
}
|
||||
if(!category.features){// Throw an error if a category is missing `features`.
|
||||
throw new Error('Could not build pricing table config from pricing-features-table.yml, the "'+category.categoryName+'" category in the yaml file is missing features. To resolve, add an array of features to this category.');
|
||||
}
|
||||
if(!_.isArray(category.features)){ // Throw an error if a category's `features`` is not an array.
|
||||
throw new Error('Could not build pricing table config from pricing-features-table.yml, The value of the "'+category.categoryName+'" category is invalid, to resolve, change the features for this category to be an array of objects.');
|
||||
}
|
||||
// Validate all features in a category.
|
||||
for(let feature of category.features){
|
||||
if(feature.name) {// Compatibility check
|
||||
throw new Error('Could not build pricing table config from pricing-features-table.yml. A feature in the "'+category.categoryName+'" category has a "name" which is no longer supported. To resolve, add a "industryName" to this feature '+feature);
|
||||
if(feature.industryName !== undefined) {
|
||||
if(!feature.industryName || typeof feature.industryName !== 'string') {
|
||||
throw new Error(`Could not build pricing table config from pricing-features-table.yml. A feature has a missing or invalid "industryName". To resolve, set an "industryName" as a valid, non-empty string for this feature ${feature}`);
|
||||
}
|
||||
if(feature.industryName !== undefined) {
|
||||
if(!feature.industryName || typeof feature.industryName !== 'string') {
|
||||
throw new Error('Could not build pricing table config from pricing-features-table.yml. A feature in the "'+category.categoryName+'" category has a missing or invalid "industryName". To resolve, set an "industryName" as a valid, non-empty string for this feature '+feature);
|
||||
feature.name = feature.industryName;//« This is just an alias. FUTURE: update code elsewhere to use the new property instead, and delete this aliasing.
|
||||
}
|
||||
if(!feature.productCategories){
|
||||
throw new Error(`Could not build pricing table config from pricing-features-table.yml. The '${feature.industryName}' feature is missing a 'productCategories' value. Please add an array of product categories to this feature and try running this script again`);
|
||||
} else {
|
||||
if(!_.isArray(feature.productCategories)){
|
||||
throw new Error(`Could not build pricing table config from pricing-features-table.yml. The '${feature.industryName}' feature has an invalid 'productCategories' value. Please change the productCategories for this feature to be an array of product categories`);
|
||||
} else {
|
||||
for(let category of feature.productCategories){
|
||||
if(!_.contains(VALID_PRODUCT_CATEGORIES, category)){
|
||||
throw new Error(`Could not build pricing table config from pricing-features-table.yml. The '${feature.industryName}' feature has a 'productCategories' with an an invalid product category (${category}). Please change the values in this array to be one of: ${VALID_PRODUCT_CATEGORIES.join(', ')}`);
|
||||
}
|
||||
}
|
||||
feature.name = feature.industryName;//« This is just an alias. FUTURE: update code elsewhere to use the new property instead, and delete this aliasing.
|
||||
}
|
||||
if(!feature.tier) { // Throw an error if a feature is missing a `tier`.
|
||||
throw new Error('Could not build pricing table config from pricing-features-table.yml. The "'+feature.industryName+'" feature is missing a "tier". To resolve, add a "tier" (either "Free" or "Premium") to this feature.');
|
||||
} else if(!_.contains(['Free', 'Premium'], feature.tier)){ // Throw an error if a feature's `tier` is not "Free" or "Premium".
|
||||
throw new Error('Could not build pricing table config from pricing-features-table.yml. The "'+feature.industryName+'" feature has an invalid "tier". to resolve, change the value of this features "tier" (currently set to '+feature.tier+') to be either "Free" or "Premium".');
|
||||
}
|
||||
if(feature.comingSoon) {// Compatibility check
|
||||
throw new Error('Could not build pricing table config from pricing-features-table.yml. A feature in the "'+category.categoryName+'" category has "comingSoon", which is no longer supported. To resolve, remove "comingSoon" or add "comingSoonOn" (YYYY-MM-DD) to this feature '+feature);
|
||||
}
|
||||
if(feature.comingSoonOn !== undefined) {
|
||||
if(typeof feature.comingSoonOn !== 'string'){
|
||||
throw new Error('Could not build pricing table config from pricing-features-table.yml. The "'+feature.industryName+'" feature has an invalid "comingSoonOn" value (currently set to '+feature.comingSoonOn+', but expecting a string like \'YYYY-MM-DD\'.)');
|
||||
}
|
||||
feature.comingSoon = true;//« This is just an alias. FUTURE: update code elsewhere to use the new property instead, and delete this aliasing.
|
||||
}//fi
|
||||
}
|
||||
if(!feature.tier) { // Throw an error if a feature is missing a `tier`.
|
||||
throw new Error(`Could not build pricing table config from pricing-features-table.yml. The ${feature.industryName} feature is missing a "tier". To resolve, add a "tier" (either "Free" or "Premium") to this feature.`);
|
||||
} else if(!_.contains(['Free', 'Premium'], feature.tier)){ // Throw an error if a feature's `tier` is not "Free" or "Premium".
|
||||
throw new Error(`Could not build pricing table config from pricing-features-table.yml. The ${feature.industryName} feature has an invalid "tier". to resolve, change the value of this features "tier" (currently set to '+feature.tier+') to be either "Free" or "Premium".`);
|
||||
}
|
||||
if(feature.comingSoon) {// Compatibility check
|
||||
throw new Error(`Could not build pricing table config from pricing-features-table.yml. A feature (industryName: ${feature.industryName}) category has "comingSoon", which is no longer supported. To resolve, remove "comingSoon" or add "comingSoonOn" (YYYY-MM-DD) to this feature ${feature}`);
|
||||
}
|
||||
if(feature.comingSoonOn !== undefined) {
|
||||
if(typeof feature.comingSoonOn !== 'string'){
|
||||
throw new Error(`Could not build pricing table config from pricing-features-table.yml. The ${feature.industryName} feature has an invalid "comingSoonOn" value (currently set to ${feature.comingSoonOn}, but expecting a string like 'YYYY-MM-DD'.)`);
|
||||
}
|
||||
feature.comingSoon = true;//« This is just an alias. FUTURE: update code elsewhere to use the new property instead, and delete this aliasing.
|
||||
}//fi
|
||||
}
|
||||
builtStaticContent.pricingTable = pricingTableCategories;
|
||||
builtStaticContent.pricingTable = pricingTableFeatures;
|
||||
},
|
||||
async()=>{
|
||||
let rituals = {};
|
||||
|
||||
Vendored
+39
-58
@@ -45,15 +45,15 @@
|
||||
</div>
|
||||
<div purpose="pricing-switch" class="d-flex flex-md-row flex-column justify-content-center mx-auto">
|
||||
<div purpose="pricing-switch-option" class="all" :class="[pricingMode === 'all' ? 'selected' : '']" @click="pricingMode = 'all'">All features</div>
|
||||
<div purpose="pricing-switch-option" class="security" :class="[pricingMode === 'security' ? 'selected' : '']" @click="pricingMode = 'security'">For security engineers</div>
|
||||
<div purpose="pricing-switch-option" class="it" :class="[pricingMode === 'it' ? 'selected' : '']" @click="pricingMode = 'it'">For IT admins</div>
|
||||
<div purpose="pricing-switch-option" class="security" :class="[pricingMode === 'Security' ? 'selected' : '']" @click="pricingMode = 'Security'">For security engineers</div>
|
||||
<div purpose="pricing-switch-option" class="it" :class="[pricingMode === 'IT' ? 'selected' : '']" @click="pricingMode = 'IT'">For IT admins</div>
|
||||
<div purpose="pricing-tier-switch" :class="pricingMode+'-selected'"></div>
|
||||
</div>
|
||||
</div>
|
||||
<%// IT-focused features table %>
|
||||
<div purpose="features-table" class="d-flex flex-column justify-content-center mx-auto px-0" v-if="pricingMode === 'all' || pricingMode === 'it'">
|
||||
<%// All features %>
|
||||
<div purpose="features-table" class="d-flex flex-column justify-content-center mx-auto px-0" v-if="pricingMode === 'all'">
|
||||
<div class="d-flex flex-column justify-content-center p-0">
|
||||
<%// Desktop IT-focused features tables %>
|
||||
<%// Desktop features tables %>
|
||||
<div class="d-none d-md-block">
|
||||
<% for(let category of pricingTable) {%>
|
||||
<table purpose="pricing-categories-table" class="table">
|
||||
@@ -93,7 +93,7 @@
|
||||
<% }%>
|
||||
<p style="color: #515774;" class="mb-0 w-100">* Coming soon</p>
|
||||
</div>
|
||||
<%// Mobile IT-focused features tables %>
|
||||
<%// Mobile features tables %>
|
||||
<% for(let category of pricingTable) {%>
|
||||
<div purpose="mobile-feature-table-section" class="d-block d-md-none">
|
||||
<h4><%- category.categoryName %></h4>
|
||||
@@ -119,72 +119,53 @@
|
||||
<p style="color: #515774;" class="d-block d-md-none mb-0 w-100">* Coming soon</p>
|
||||
</div>
|
||||
</div>
|
||||
<%// Security-focused features table %>
|
||||
<div purpose="features-table" class="d-flex flex-column justify-content-center mx-auto px-0" v-else-if="pricingMode === 'security'">
|
||||
<%// Filtered features %>
|
||||
<div purpose="features-table" class="d-flex flex-column justify-content-center mx-auto px-0" v-else>
|
||||
<div class="d-flex flex-column justify-content-center p-0">
|
||||
<%// Desktop security-focused features tables %>
|
||||
<%// Desktop filtered features tables %>
|
||||
<div class="d-none d-md-block">
|
||||
<% for(let category of pricingTableForSecurity) {%>
|
||||
<table purpose="pricing-categories-table" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td><h4><%- category.categoryName %></h4></td>
|
||||
<%if(pricingTableForSecurity.indexOf(category) === 0) {%>
|
||||
<td class="text-center">
|
||||
<strong>Free</strong>
|
||||
</td>
|
||||
<%}else {%>
|
||||
<td ></td>
|
||||
<%}%>
|
||||
<%if(pricingTableForSecurity.indexOf(category) === 0) {%>
|
||||
<td class="text-center">
|
||||
<strong>Premium</strong>
|
||||
</td>
|
||||
<%}else {%>
|
||||
<td ></td>
|
||||
<%}%>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% for( let feature of category.features){ %>
|
||||
<tr>
|
||||
<td purpose="feature-name" class="w-50"><%- feature.name %><%- feature.comingSoon ? '*' : '' %></td>
|
||||
<%if(feature.tier === 'Free') {%>
|
||||
<td purpose="table-checkmark"><img class="mx-auto" alt="checkmark" purpose="checkmark" src="/images/icon-checkmark-green-16x16@2x.png"></td>
|
||||
<%} else {%>
|
||||
<td purpose="table-checkmark"></td>
|
||||
<%}%>
|
||||
<td purpose="table-checkmark"><img class="mx-auto" alt="checkmark" purpose="checkmark" src="/images/icon-checkmark-green-16x16@2x.png"></td>
|
||||
</tr>
|
||||
<% }%>
|
||||
</tbody>
|
||||
</table>
|
||||
<% }%>
|
||||
<table purpose="pricing-categories-table" class="table" v-for="(category, index) in pricingTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<td><h4>{{category.categoryName}}</h4></td>
|
||||
<td class="text-center" v-if="index === 0">
|
||||
<strong>Free</strong>
|
||||
</td>
|
||||
<td v-else></td>
|
||||
<td class="text-center" v-if="index === 0">
|
||||
<strong>Premium</strong>
|
||||
</td>
|
||||
<td v-else></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="feature in category.features" v-if="_.contains([pricingMode], feature.usualDepartment)">
|
||||
<td purpose="feature-name">{{feature.name}}{{feature.comingSoon ? '*' : ''}}</td>
|
||||
<td purpose="table-checkmark" v-if="feature.tier === 'Free'"><img class="mx-auto" alt="checkmark" purpose="checkmark" src="/images/icon-checkmark-green-16x16@2x.png"></td>
|
||||
<td purpose="table-checkmark" v-else></td>
|
||||
<td purpose="table-checkmark"><img class="mx-auto" alt="checkmark" purpose="checkmark" src="/images/icon-checkmark-green-16x16@2x.png"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="color: #515774;" class="mb-0 w-100">* Coming soon</p>
|
||||
</div>
|
||||
<%// Mobile security-focused features tables %>
|
||||
<% for(let category of pricingTableForSecurity) {%>
|
||||
<div purpose="mobile-feature-table-section" class="d-block d-md-none">
|
||||
<h4><%- category.categoryName %></h4>
|
||||
<% for( let feature of category.features){ %>
|
||||
<div purpose="mobile-features-table" class="d-flex flex-column">
|
||||
<div purpose="striped-row" class="d-flex flex-row justify-content-between align-items-center">
|
||||
<strong><%- feature.name %><%- feature.comingSoon ? '*' : '' %></strong>
|
||||
<%// Mobile filtered features tables %>
|
||||
<div purpose="mobile-feature-table-section" class="d-block d-md-none" v-for="category in pricingTable">
|
||||
<h4>{{category.categoryName}}</h4>
|
||||
<div purpose="mobile-features-table" class="d-flex flex-column" v-for="feature in category.features">
|
||||
<div purpose="striped-row" class="d-flex flex-row justify-content-between align-items-center" v-if="_.contains([pricingMode], feature.usualDepartment)">
|
||||
<strong>{{feature.name}}{{ feature.comingSoon ? '*' : ''}}</strong>
|
||||
</div>
|
||||
<div class="d-flex flex-row justify-content-between align-items-center">
|
||||
Free
|
||||
<%if(feature.tier === 'Free') {%>
|
||||
<img class="d-block" alt="checkmark" purpose="checkmark" src="/images/icon-checkmark-green-16x16@2x.png">
|
||||
<% } %>
|
||||
<img class="d-block" alt="checkmark" purpose="checkmark" src="/images/icon-checkmark-green-16x16@2x.png" v-if="feature.tier === 'Free'">
|
||||
</div>
|
||||
<div purpose="striped-row" class="d-flex flex-row justify-content-between align-items-center">
|
||||
Premium
|
||||
<img class="d-block" alt="checkmark" purpose="checkmark" src="/images/icon-checkmark-green-16x16@2x.png">
|
||||
</div>
|
||||
</div>
|
||||
<% }%>
|
||||
</div>
|
||||
<% }%>
|
||||
<p style="color: #515774;" class="d-block d-md-none mb-0 w-100">* Coming soon</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user