Skip to content

Commit bb43022

Browse files
Merge pull request #1538 from appknox/PD-1717-sbom-upselling-ui
sbom upselling UI
2 parents a44108d + 90be5d3 commit bb43022

File tree

19 files changed

+364
-12
lines changed

19 files changed

+364
-12
lines changed

app/components/appknox-wrapper/index.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,7 @@ export default class AppknoxWrapperComponent extends Component<AppknoxWrapperSig
9393
}
9494

9595
get showSbomDashboard() {
96-
return (
97-
this.organization.selected?.features?.sbom &&
98-
!this.configuration.serverData.enterprise
99-
);
96+
return !this.configuration.serverData.enterprise;
10097
}
10198

10299
get menuItems() {

app/components/file/report-drawer/sbom-reports/sample/index.ts

+6
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ export default class FileReportDrawerSbomReportsSampleComponent extends Componen
3333
constructor(owner: unknown, args: object) {
3434
super(owner, args);
3535

36+
const storedState = this.window.localStorage.getItem('sbomRequest');
37+
38+
this.hasContactedSupport = storedState === 'true';
39+
3640
this.fetchSampleReport.perform();
3741
}
3842

@@ -59,6 +63,8 @@ export default class FileReportDrawerSbomReportsSampleComponent extends Componen
5963
try {
6064
await waitForPromise(this.ajax.post(this.CONTACT_SUPPORT_ENDPOINT));
6165

66+
this.window.localStorage.setItem('sbomRequest', 'true');
67+
6268
this.hasContactedSupport = true;
6369
} catch (err) {
6470
this.notify.error(parseError(err, this.tPleaseTryAgain));
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<UpsellingModule
2+
@title={{t 'sbomModule.introducingSBOM'}}
3+
@subtitle={{t 'sbomModule.sbomOverview'}}
4+
@features={{this.features}}
5+
@successMessage={{t 'sbomModule.sbomContactMessage'}}
6+
@showSuccess={{this.hasContactedSupport}}
7+
@onClickContactCTA={{this.onClickContactCTA}}
8+
>
9+
<:gradientContent>
10+
<img
11+
data-test-sbom-upselling-image
12+
height='274px'
13+
width='434px'
14+
src='/images/sbom-upselling.png'
15+
alt='sbom-feature-request'
16+
/>
17+
</:gradientContent>
18+
</UpsellingModule>
+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import Component from '@glimmer/component';
2+
import { tracked } from '@glimmer/tracking';
3+
import { action } from '@ember/object';
4+
import { service } from '@ember/service';
5+
import { waitForPromise } from '@ember/test-waiters';
6+
import { task } from 'ember-concurrency';
7+
import type IntlService from 'ember-intl/services/intl';
8+
9+
import parseError from 'irene/utils/parse-error';
10+
import type IreneAjaxService from 'irene/services/ajax';
11+
12+
export default class SbomUpsellingComponent extends Component {
13+
@service declare intl: IntlService;
14+
@service declare ajax: IreneAjaxService;
15+
@service('notifications') declare notify: NotificationService;
16+
@service('browser/window') declare window: Window;
17+
18+
@tracked hasContactedSupport = false;
19+
20+
CONTACT_SUPPORT_ENDPOINT = 'v2/feature_request/sbom';
21+
22+
constructor(owner: unknown, args: object) {
23+
super(owner, args);
24+
25+
const storedState = this.window.localStorage.getItem('sbomRequest');
26+
27+
this.hasContactedSupport = storedState === 'true';
28+
}
29+
30+
get features() {
31+
return [
32+
this.intl.t('sbomModule.sbomFeatures.oneClickAnalysis'),
33+
this.intl.t('sbomModule.sbomFeatures.identifyDependencies'),
34+
this.intl.t('sbomModule.sbomFeatures.uncoverVulnerabilities'),
35+
this.intl.t('sbomModule.sbomFeatures.componentInsights'),
36+
];
37+
}
38+
39+
@action
40+
onClickContactCTA() {
41+
this.contactSupport.perform();
42+
}
43+
44+
contactSupport = task(async () => {
45+
try {
46+
await waitForPromise(this.ajax.post(this.CONTACT_SUPPORT_ENDPOINT));
47+
48+
this.window.localStorage.setItem('sbomRequest', 'true');
49+
50+
this.hasContactedSupport = true;
51+
} catch (err) {
52+
this.notify.error(parseError(err, this.intl.t('pleaseTryAgain')));
53+
}
54+
});
55+
}
56+
57+
declare module '@glint/environment-ember-loose/registry' {
58+
export default interface Registry {
59+
'Sbom::Upselling': typeof SbomUpsellingComponent;
60+
}
61+
}
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<AkStack
2+
data-test-upselling-module
3+
local-class='upsell-container'
4+
@direction='column'
5+
>
6+
<AkStack
7+
@width='full'
8+
@alignItems='center'
9+
@justifyContent='center'
10+
local-class='gradient-fill'
11+
>
12+
{{yield to='gradientContent'}}
13+
</AkStack>
14+
15+
<AkStack local-class='upsell-content' @direction='column' @spacing='2'>
16+
<AkTypography data-test-upselling-module-title @variant='h5'>
17+
{{@title}}
18+
</AkTypography>
19+
20+
<AkTypography data-test-upselling-module-subtitle>
21+
{{@subtitle}}
22+
</AkTypography>
23+
24+
<AkDivider @color='dark' />
25+
26+
{{#each @features as |feature|}}
27+
<AkStack data-test-upselling-module-feature @spacing='1'>
28+
<AkIcon @iconName='beenhere' @variant='filled' @color='success' />
29+
<AkTypography>{{feature}}</AkTypography>
30+
</AkStack>
31+
{{/each}}
32+
</AkStack>
33+
34+
<AkStack
35+
@spacing='1'
36+
@width='full'
37+
@alignItems='center'
38+
local-class='upsell-footer'
39+
>
40+
{{#if @showSuccess}}
41+
<AkSvg::ThumbsUpIcon
42+
data-test-upselling-module-thumbs-up
43+
local-class='thumbsup-icon'
44+
/>
45+
46+
<AkTypography data-test-upselling-module-successMessage>
47+
{{@successMessage}}
48+
</AkTypography>
49+
{{else}}
50+
<AkButton
51+
data-test-upselling-module-contact-cta
52+
{{on 'click' @onClickContactCTA}}
53+
@color='primary'
54+
>
55+
{{t 'imInterested'}}
56+
</AkButton>
57+
{{/if}}
58+
</AkStack>
59+
</AkStack>
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
.gradient-fill {
2+
height: 375px;
3+
background: linear-gradient(0deg, var(--upselling-module-gradient-light) 0%, var(--upselling-module-gradient) 69.79%);
4+
}
5+
6+
.upsell-container {
7+
padding: 0.714em 0 0;
8+
background-color: var(--upselling-module-background-color);
9+
box-shadow: var(--upselling-module-box-shadow-6);
10+
}
11+
12+
.upsell-content {
13+
padding: 2.5em 5em;
14+
}
15+
16+
.upsell-footer {
17+
box-shadow: var(--upselling-module-box-shadow-4);
18+
background-color: var(--upselling-module-background-color);
19+
padding: 1em 5em;
20+
}
21+
22+
.thumbsup-icon {
23+
height: 26px;
24+
width: 26px;
25+
}
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import Component from '@glimmer/component';
2+
3+
export interface UpsellingModuleSignature {
4+
Args: {
5+
title: string;
6+
subtitle: string;
7+
features: string[];
8+
onClickContactCTA: () => void;
9+
showSuccess: boolean;
10+
successMessage: string;
11+
};
12+
Blocks: {
13+
gradientContent: [];
14+
};
15+
}
16+
17+
export default class UpsellingModuleComponent extends Component<UpsellingModuleSignature> {}
18+
19+
declare module '@glint/environment-ember-loose/registry' {
20+
export default interface Registry {
21+
UpsellingModule: typeof UpsellingModuleComponent;
22+
}
23+
}

app/routes/authenticated/dashboard/sbom.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ export default class AuthenticatedDashboardSbomRoute extends Route {
77
@service declare organization: OrganizationService;
88
@service declare router: RouterService;
99

10-
beforeModel() {
11-
if (!this.organization.selected?.features?.sbom) {
12-
this.router.transitionTo('authenticated.dashboard.projects');
13-
}
10+
model() {
11+
const isSbomEnabled = this.organization.selected?.features?.sbom;
12+
13+
return { isSbomEnabled };
1414
}
1515
}

app/styles/_component-variables.scss

+5
Original file line numberDiff line numberDiff line change
@@ -2177,6 +2177,11 @@ body {
21772177
--home-page-product-card-beta-tag-bg: var(--storeknox-beta-tag-bg);
21782178
--home-page-product-card-beta-tag-color: var(--storeknox-beta-tag-color);
21792179

2180+
// variables for upselling-module
2181+
--upselling-module-background-color: var(--background-main);
2182+
--upselling-module-box-shadow-6: var(--box-shadow-6);
2183+
--upselling-module-box-shadow-4: var(--box-shadow-4);
2184+
21802185
&.theme-light {
21812186
//variables for appknox-wrapper
21822187
--appknox-wrapper-text-color: var(--text-primary);

app/styles/_icons.scss

+4
Original file line numberDiff line numberDiff line change
@@ -634,3 +634,7 @@
634634
.ak-icon-arrow-outward {
635635
@extend .mi-arrow-outward;
636636
}
637+
638+
.ak-icon-beenhere {
639+
@extend .mi-beenhere;
640+
}

app/styles/_theme.scss

+4
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@
120120
--upselling-feature-footer-bg-color: #f7f3ff;
121121
--upselling-feature-footer-color: #3e0e8c;
122122

123+
// special case variable for upselling module
124+
--upselling-module-gradient: #350B6C;
125+
--upselling-module-gradient-light: #6715D2;
126+
123127
// special case variable for storeknox
124128
--storeknox-feature-in-progress-button-color: #3e0e8c;
125129
--storeknox-feature-in-progress-button-bg-color: #f7f3ff;
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
{{page-title 'SBOM'}}
22

33
<PageWrapper @backgroundColor='inherit'>
4-
{{outlet}}
4+
{{#if @model.isSbomEnabled}}
5+
{{outlet}}
6+
{{else}}
7+
<Sbom::Upselling />
8+
{{/if}}
59
</PageWrapper>

public/images/sbom-upselling.png

128 KB
Loading

tests/integration/components/appknox-wrapper-test.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,9 @@ module('Integration | Component | appknox-wrapper', function (hooks) {
352352
async function (assert, scenario) {
353353
const { owner, admin, ...sectionConfig } = scenario;
354354
const me = this.owner.lookup('service:me');
355+
const configuration = this.owner.lookup('service:configuration');
356+
357+
configuration.serverData.enterprise = false;
355358

356359
me.org.is_owner = owner;
357360
me.org.is_admin = admin;
@@ -364,7 +367,6 @@ module('Integration | Component | appknox-wrapper', function (hooks) {
364367
this.organization.selected.features = {
365368
app_monitoring: sectionConfig.appMonitoring,
366369
public_apis: sectionConfig.publicApis,
367-
sbom: sectionConfig.sbom,
368370
};
369371

370372
await render(hbs`<AppknoxWrapper @user={{this.user}} />`);
@@ -387,6 +389,7 @@ module('Integration | Component | appknox-wrapper', function (hooks) {
387389
...sectionConfig,
388390
analytics: owner || admin,
389391
billing: owner && sectionConfig.billing,
392+
sbom: true,
390393
}).forEach((it, index) => {
391394
if (it.icon) {
392395
assert

tests/integration/components/file/report-drawer/sbom-reports/index-test.js

+6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { find, findAll, render, waitFor, waitUntil } from '@ember/test-helpers';
66
import { hbs } from 'ember-cli-htmlbars';
77
import Service from '@ember/service';
88
import { SbomReportStatus } from 'irene/models/sbom-report';
9+
import { setupBrowserFakes } from 'ember-browser-services/test-support';
910

1011
class OrganizationStub extends Service {
1112
selected = {
@@ -22,12 +23,17 @@ module(
2223
setupRenderingTest(hooks);
2324
setupMirage(hooks);
2425
setupIntl(hooks, 'en');
26+
setupBrowserFakes(hooks, { window: true, localStorage: true });
2527

2628
hooks.beforeEach(async function () {
2729
this.owner.register('service:organization', OrganizationStub);
2830

2931
const store = this.owner.lookup('service:store');
3032

33+
const window = this.owner.lookup('service:browser/window');
34+
35+
window.localStorage.clear();
36+
3137
const createFile = (sbomId) => {
3238
if (!sbomId && sbomId !== null) {
3339
sbomId = this.server.create('sbom-file').id;

tests/integration/components/file/report-drawer/sbom-reports/sample-test.js

+31-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,22 @@ class WindowStub extends Service {
1919
url = null;
2020
target = null;
2121

22+
localStorage = {
23+
storage: new Map(),
24+
25+
getItem(key) {
26+
return this.storage.get(key) ?? null;
27+
},
28+
29+
setItem(key, value) {
30+
this.storage.set(key, value);
31+
},
32+
33+
clear() {
34+
this.storage.clear();
35+
},
36+
};
37+
2238
open(url, target) {
2339
this.url = url;
2440
this.target = target;
@@ -53,8 +69,16 @@ module(
5369
return schema.sbomReports.find(`${req.params.id}`)?.toJSON();
5470
});
5571

56-
this.server.post('/v2/feature_request/sbom', (schema, req) => {
57-
return schema.sbomReports.find(`${req.params.id}`)?.toJSON();
72+
this.server.post('/v2/feature_request/sbom', () => {
73+
return 200;
74+
});
75+
76+
const window = this.owner.lookup('service:browser/window');
77+
78+
window.localStorage.clear();
79+
80+
this.setProperties({
81+
window,
5882
});
5983
});
6084

@@ -101,6 +125,11 @@ module(
101125

102126
await click('[data-test-sbomReports-contactUsButton]');
103127

128+
assert.strictEqual(
129+
this.window.localStorage.getItem('sbomRequest'),
130+
'true'
131+
);
132+
104133
assert.dom('[data-test-sbomReports-contactUsButton]').doesNotExist();
105134
assert.dom('[data-test-sbomReports-contactSuccessIllustration]').exists();
106135

0 commit comments

Comments
 (0)