diff --git a/app/adapters/sk-inventory-approval-status.ts b/app/adapters/sk-inventory-approval-status.ts deleted file mode 100644 index 2587b0049..000000000 --- a/app/adapters/sk-inventory-approval-status.ts +++ /dev/null @@ -1,15 +0,0 @@ -import CommonDRFAdapter from './commondrf'; - -export default class UnknownAnalysisStatus extends CommonDRFAdapter { - urlForQueryRecord() { - const baseurl = `${this.namespace_v2}/sk_app/check_approval_status`; - - return this.buildURLFromBase(baseurl); - } -} - -declare module 'ember-data/types/registries/adapter' { - export default interface AdapterRegistry { - 'sk-inventory-approval-status': UnknownAnalysisStatus; - } -} diff --git a/app/components/ak-svg/aox-icon.hbs b/app/components/ak-svg/aox-icon.hbs index 7cd166635..15a4d453d 100644 --- a/app/components/ak-svg/aox-icon.hbs +++ b/app/components/ak-svg/aox-icon.hbs @@ -4,6 +4,7 @@ viewBox='0 0 33 32' fill='none' xmlns='http://www.w3.org/2000/svg' + ...attributes > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/components/ak-svg/sox-monitoring-pending.hbs b/app/components/ak-svg/sox-monitoring-results-pending.hbs similarity index 99% rename from app/components/ak-svg/sox-monitoring-pending.hbs rename to app/components/ak-svg/sox-monitoring-results-pending.hbs index 00036292b..329cd817d 100644 --- a/app/components/ak-svg/sox-monitoring-pending.hbs +++ b/app/components/ak-svg/sox-monitoring-results-pending.hbs @@ -4,6 +4,7 @@ viewBox='0 0 210 118' fill='none' xmlns='http://www.w3.org/2000/svg' + ...attributes > - - - - - {{#if column.headerComponent}} - {{#let (component column.headerComponent) as |Component|}} - - {{/let}} - {{else}} - {{column.name}} - {{/if}} - - - - - - - {{#let (component r.columnValue.cellComponent) as |Component|}} - - {{/let}} - - - - - - - \ No newline at end of file diff --git a/app/components/storeknox/discover/requested-apps/table/index.scss b/app/components/storeknox/discover/requested-apps/table/index.scss deleted file mode 100644 index e1718b08d..000000000 --- a/app/components/storeknox/discover/requested-apps/table/index.scss +++ /dev/null @@ -1,8 +0,0 @@ -.requested-table { - tr { - background-color: var(--storeknox-discover-requested-apps-table-row-color); - border: 1px solid - var(--storeknox-discover-requested-apps-table-row-border-color); - border-top: 0; - } -} diff --git a/app/components/storeknox/discover/requested-apps/table/index.ts b/app/components/storeknox/discover/requested-apps/table/index.ts deleted file mode 100644 index 46c63d270..000000000 --- a/app/components/storeknox/discover/requested-apps/table/index.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { action } from '@ember/object'; -import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; -import { inject as service } from '@ember/service'; -import RouterService from '@ember/routing/router-service'; -import IntlService from 'ember-intl/services/intl'; - -interface LimitOffset { - limit: number; - offset: number; -} - -export default class StoreknoxDiscoverRequestedAppsTableComponent extends Component { - @service declare router: RouterService; - @service declare intl: IntlService; - - @tracked requestedApps = [ - { - isAndroid: true, - iconUrl: - 'https://appknox-production-public.s3.amazonaws.com/908e507e-1148-4f4d-9939-6dba3d645abc.png', - name: 'Shell Asia', - packageName: 'com.shellasia.android', - companyName: 'Shell Information Technology International', - mailId: 'asiashell@shell.com', - status: 'pending', - }, - { - isAndroid: true, - iconUrl: - 'https://appknox-production-public.s3.amazonaws.com/908e507e-1148-4f4d-9939-6dba3d645abc.png', - name: 'Shell Recharge India', - packageName: 'com.shellrecharge.india', - companyName: 'Shell Information Technology International', - mailId: null, - status: 'approved', - actionTakenBy: 'subho', - }, - { - isIos: true, - iconUrl: - 'https://appknox-production-public.s3.amazonaws.com/908e507e-1148-4f4d-9939-6dba3d645abc.png', - name: 'Shell Mobility Site Manager', - packageName: 'com.shellmobility.ios', - companyName: 'Shell Information Technology International', - mailId: null, - requested: true, - status: 'rejected', - actionTakenBy: 'subho', - }, - ]; - - // Table Actions - @action goToPage(args: LimitOffset) { - const { limit, offset } = args; - this.router.transitionTo('authenticated.storeknox.discover.requested', { - queryParams: { app_limit: limit, app_offset: offset }, - }); - } - - @action onItemPerPageChange(args: LimitOffset) { - const { limit } = args; - const offset = 0; - - this.router.transitionTo('authenticated.storeknox.discover.requested', { - queryParams: { app_limit: limit, app_offset: offset }, - }); - } - - get totalCount() { - return this.requestedApps.length || 0; - } - - get tableData() { - return this.requestedApps; - } - - get itemPerPageOptions() { - return [10, 25, 50]; - } - - get limit() { - return 10; - } - - get offset() { - return 0; - } - - get columns() { - return [ - { - headerComponent: 'storeknox/table-columns/store-header', - cellComponent: 'storeknox/table-columns/store', - minWidth: 30, - width: 30, - textAlign: 'center', - }, - { - name: this.intl.t('application'), - cellComponent: 'storeknox/table-columns/application', - }, - { - name: this.intl.t('developer'), - cellComponent: 'storeknox/table-columns/developer', - }, - { - name: this.intl.t('status'), - cellComponent: 'storeknox/discover/requested-apps/table/status', - width: 80, - }, - ]; - } -} - -declare module '@glint/environment-ember-loose/registry' { - export default interface Registry { - 'Storeknox::Discover::RequestedApps::Table': typeof StoreknoxDiscoverRequestedAppsTableComponent; - } -} diff --git a/app/components/storeknox/discover/requested-apps/table/status/index.hbs b/app/components/storeknox/discover/requested-apps/table/status/index.hbs deleted file mode 100644 index 86e9f97de..000000000 --- a/app/components/storeknox/discover/requested-apps/table/status/index.hbs +++ /dev/null @@ -1,69 +0,0 @@ -{{#if (eq @data.status 'pending')}} - - <:icon> - - - -{{else if (eq @data.status 'approved')}} - - - {{t 'storeknox.approved'}} - - - - - {{t 'by'}} - {{@data.actionTakenBy}} - - - - <:tooltipContent> - - - - - June 17, 2024, 12:51 - - - - - <:default> - - - - - -{{else if (eq @data.status 'rejected')}} - - - {{t 'rejected'}} - - - - - {{t 'by'}} - {{@data.actionTakenBy}} - - - - <:tooltipContent> - - - - - June 17, 2024, 12:51 - - - - - <:default> - - - - - -{{/if}} \ No newline at end of file diff --git a/app/components/storeknox/discover/requested-apps/table/status/index.scss b/app/components/storeknox/discover/requested-apps/table/status/index.scss deleted file mode 100644 index 761831828..000000000 --- a/app/components/storeknox/discover/requested-apps/table/status/index.scss +++ /dev/null @@ -1,5 +0,0 @@ -.info-icon { - font-size: 1em !important; - height: 12px; - color: var(--storeknox-discover-requested-apps-table-status-info-icon-color); -} diff --git a/app/components/storeknox/inventory-details/app-details/actions-list/index.hbs b/app/components/storeknox/inventory-details/app-details/actions-list/index.hbs index ec0664cda..4928fe171 100644 --- a/app/components/storeknox/inventory-details/app-details/actions-list/index.hbs +++ b/app/components/storeknox/inventory-details/app-details/actions-list/index.hbs @@ -4,7 +4,11 @@ {{style height='50px'}} > <:icon> - + <:default> @@ -13,16 +17,28 @@ @justifyContent='space-between' @alignItems='center' > - + {{t 'storeknox.actionNeeded'}} ({{this.actionableItemsCount}}) - + - + - + {{this.lastMonitoredDate}} @@ -42,6 +58,7 @@ @needsAction={{action.needsAction}} @featureInProgress={{action.featureInProgress}} @label={{action.label}} + data-test-storeknoxInventoryDetails-actionBtn /> {{/each}} diff --git a/app/components/storeknox/inventory-details/app-details/actions-list/index.ts b/app/components/storeknox/inventory-details/app-details/actions-list/index.ts index d2b18f9ba..34fe87e22 100644 --- a/app/components/storeknox/inventory-details/app-details/actions-list/index.ts +++ b/app/components/storeknox/inventory-details/app-details/actions-list/index.ts @@ -8,7 +8,7 @@ import type SkInventoryAppModel from 'irene/models/sk-inventory-app'; interface StoreknoxInventoryDetailsAppDetailsActionsListSignature { Args: { - app?: SkInventoryAppModel; + skInventoryApp?: SkInventoryAppModel; }; } @@ -16,7 +16,7 @@ export default class StoreknoxInventoryDetailsAppDetailsActionsListComponent ext @service declare intl: IntlService; get skInventoryApp() { - return this.args.app; + return this.args.skInventoryApp; } get actionsList() { diff --git a/app/components/storeknox/inventory-details/app-details/app-info/index.hbs b/app/components/storeknox/inventory-details/app-details/app-info/index.hbs new file mode 100644 index 000000000..d1f52fed8 --- /dev/null +++ b/app/components/storeknox/inventory-details/app-details/app-info/index.hbs @@ -0,0 +1,48 @@ + + + <:default> + + {{t 'storeknox.appDetails'}} + + + + + + + {{#each this.appDetailsInfo as |info idx|}} + {{#if (gt idx 0)}} + + {{/if}} + + + + {{info.title}} + + + + {{or info.value '-'}} + + + {{/each}} + + + \ No newline at end of file diff --git a/app/components/storeknox/inventory-details/app-details/app-info/index.ts b/app/components/storeknox/inventory-details/app-details/app-info/index.ts new file mode 100644 index 000000000..f279c08e7 --- /dev/null +++ b/app/components/storeknox/inventory-details/app-details/app-info/index.ts @@ -0,0 +1,42 @@ +import { service } from '@ember/service'; +import Component from '@glimmer/component'; +import dayjs from 'dayjs'; + +import type IntlService from 'ember-intl/services/intl'; +import type SkInventoryAppModel from 'irene/models/sk-inventory-app'; + +interface StoreknoxInventoryDetailsAppDetailsAppInfoSignature { + Args: { + skInventoryApp: SkInventoryAppModel; + }; +} + +export default class StoreknoxInventoryDetailsAppDetailsAppInfoComponent extends Component { + @service declare intl: IntlService; + + get appDetailsInfo() { + return [ + { + id: 'developer', + title: this.intl.t('storeknox.developer'), + value: this.args.skInventoryApp?.devName, + }, + { + id: 'email-id', + title: this.intl.t('emailId'), + value: this.args.skInventoryApp?.devEmail, + }, + { + id: 'date-added', + title: this.intl.t('storeknox.addedToInventoryOn'), + value: dayjs(this.args.skInventoryApp?.addedOn).format('DD, MMM YYYY'), + }, + ]; + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Storeknox::InventoryDetails::AppDetails::AppInfo': typeof StoreknoxInventoryDetailsAppDetailsAppInfoComponent; + } +} diff --git a/app/components/storeknox/inventory-details/app-details/index.hbs b/app/components/storeknox/inventory-details/app-details/index.hbs index 03f8da6b0..8b1f39f1c 100644 --- a/app/components/storeknox/inventory-details/app-details/index.hbs +++ b/app/components/storeknox/inventory-details/app-details/index.hbs @@ -1,324 +1,20 @@ - {{#if this.skInventoryApp.monitoringStatusIsPending}} - - + {{#if @skInventoryApp.monitoringStatusIsPending}} + - - {{t 'storeknox.monitoringResultsPending'}} - - {{else}} {{/if}} - - - <:default> - - {{t 'storeknox.appDetails'}} - - - - - - - {{#each this.appDetailsInfo as |info idx|}} - {{#if (gt idx 0)}} - - {{/if}} - - - - {{info.title}} - - - - {{or info.value '-'}} - - - {{/each}} - - - - - {{#if this.skInventoryApp.appIsNotAvailableOnAppknox}} - - - <:default> - - {{t 'storeknox.latestVAResults'}} - - - - - - - - - - {{t 'storeknox.appNotInAppknoxHeader'}} - - - - {{t 'storeknox.appNotInAppknoxDescription'}} - - - - - <:leftIcon> - - - - <:default> - {{t 'appMonitoringModule.initiateUpload'}} - - - - - - {{else if this.skInventoryApp.coreProjectLatestVersion}} - - - <:default> - - {{t 'storeknox.latestVAResults'}} - - - - - - - - - {{t 'summary'}} - - - - - - - - - - {{t 'fileID'}} - - - - {{this.skInventoryApp.coreProjectLatestVersion.id}} - - - - {{#each this.vaResultsData as |info|}} - - - - - {{info.title}} - - - - {{info.value}} - - - {{/each}} - - - - {{/if}} - - {{! LOADING STATE FOR VA RESULT }} - {{! TODO: Remove is unneeded }} - {{!-- - - <:default> - - {{t 'storeknox.latestVAResults'}} - - - - - - - - - {{t 'summary'}} - - - - - - - - - - {{t 'fileID'}} - - - - - - {{#each this.vaResultsData as |info|}} - - - - - {{info.title}} - - - - - {{/each}} - - - --}} - - {{! UPLOAD COMPLETE STATE - INITIATE UPLOAD }} - {{!-- - - <:default> - - {{t 'storeknox.latestVAResults'}} - - - - - - - - - Completed... - 100% - - - - - --}} - - {{! UPLOAD IN PROGRESS STATE - INITIATE UPLOAD }} - {{!-- - - <:default> - - {{t 'storeknox.latestVAResults'}} - - - - - - - - - Processing... - 70% - - - - - - --}} - - {{! INSUFFICIENT CREDITS - INITIATE UPLOAD }} - {{!-- - - <:default> - - {{t 'storeknox.latestVAResults'}} - - - - - - - - - - Insufficient Credits - - - - We tried to upload, due to Insufficient Credits. The upload has been - cancelled. - - + - - Contact Support - - - --}} + \ No newline at end of file diff --git a/app/components/storeknox/inventory-details/app-details/index.ts b/app/components/storeknox/inventory-details/app-details/index.ts index 41161424c..06526b0a4 100644 --- a/app/components/storeknox/inventory-details/app-details/index.ts +++ b/app/components/storeknox/inventory-details/app-details/index.ts @@ -1,9 +1,4 @@ import Component from '@glimmer/component'; -import dayjs from 'dayjs'; -import { inject as service } from '@ember/service'; - -import type IntlService from 'ember-intl/services/intl'; -import type Store from '@ember-data/store'; import type SkInventoryAppModel from 'irene/models/sk-inventory-app'; interface StoreknoxInventoryDetailsAppDetailsSignature { @@ -12,55 +7,7 @@ interface StoreknoxInventoryDetailsAppDetailsSignature { }; } -export default class StoreknoxInventoryDetailsAppDetailsComponent extends Component { - @service declare intl: IntlService; - @service declare store: Store; - @service('notifications') declare notify: NotificationService; - - get skInventoryApp() { - return this.args.skInventoryApp; - } - - get coreProjectLatestVersion() { - return this.skInventoryApp?.coreProjectLatestVersion; - } - - get appDetailsInfo() { - return [ - { - title: this.intl.t('storeknox.developer'), - value: this.skInventoryApp?.devName, - }, - { - title: this.intl.t('emailId'), - value: this.skInventoryApp?.devEmail, - }, - { - title: this.intl.t('storeknox.addedToInventoryOn'), - value: dayjs(this.skInventoryApp?.addedOn).format('DD, MMM YYYY'), - }, - ]; - } - - get vaResultsData() { - return [ - { - title: this.intl.t('version'), - value: this.coreProjectLatestVersion?.get('version'), - }, - { - title: this.intl.t('versionCodeTitleCase'), - value: this.coreProjectLatestVersion?.get('versionCode'), - }, - { - title: this.intl.t('storeknox.lastScannedDate'), - value: dayjs(this.coreProjectLatestVersion?.get('createdOn')).format( - 'DD MMM YYYY' - ), - }, - ]; - } -} +export default class StoreknoxInventoryDetailsAppDetailsComponent extends Component {} declare module '@glint/environment-ember-loose/registry' { export default interface Registry { diff --git a/app/components/storeknox/inventory-details/app-details/monitoring-pending-info/index.hbs b/app/components/storeknox/inventory-details/app-details/monitoring-pending-info/index.hbs new file mode 100644 index 000000000..dcd2c9b9c --- /dev/null +++ b/app/components/storeknox/inventory-details/app-details/monitoring-pending-info/index.hbs @@ -0,0 +1,50 @@ + + + {{#if @skInventoryApp.monitoringEnabled}} + + + + {{t 'storeknox.monitoringResultsPending'}} + + + {{else}} + + + + + {{t 'storeknox.enableMonitoringForApp'}} + + + + {{t 'storeknox.monitoringResultsPendingWithMonitoringOff'}} + + + {{/if}} + \ No newline at end of file diff --git a/app/components/storeknox/inventory-details/app-details/monitoring-pending-info/index.ts b/app/components/storeknox/inventory-details/app-details/monitoring-pending-info/index.ts new file mode 100644 index 000000000..3ce7ed413 --- /dev/null +++ b/app/components/storeknox/inventory-details/app-details/monitoring-pending-info/index.ts @@ -0,0 +1,16 @@ +import Component from '@glimmer/component'; +import type SkInventoryAppModel from 'irene/models/sk-inventory-app'; + +interface StoreknoxInventoryDetailsAppDetailsMonitoringPendingInfoSignature { + Args: { + skInventoryApp: SkInventoryAppModel; + }; +} + +export default class StoreknoxInventoryDetailsAppDetailsMonitoringPendingInfoComponent extends Component {} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Storeknox::InventoryDetails::AppDetails::MonitoringPendingInfo': typeof StoreknoxInventoryDetailsAppDetailsMonitoringPendingInfoComponent; + } +} diff --git a/app/components/storeknox/inventory-details/app-details/va-results/index.hbs b/app/components/storeknox/inventory-details/app-details/va-results/index.hbs index d243924fe..42e7203d7 100644 --- a/app/components/storeknox/inventory-details/app-details/va-results/index.hbs +++ b/app/components/storeknox/inventory-details/app-details/va-results/index.hbs @@ -1,17 +1,393 @@ - - {{#each this.vaResultsCategories as |reCat|}} - - - + + + <:default> + + {{t 'storeknox.latestVAResults'}} + + + + + {{#if this.fetchCoreProjectLatestVersion.isRunning}} + + + + + {{t 'summary'}} + + + + + + + + + + {{t 'fileID'}} + + + + + + {{#each this.vaResultsData as |info|}} + + + + + {{info.title}} + + + + + {{/each}} + + + + {{! TODO: Update illustration and message to display }} + {{else if this.fileNotAccessibleToUser}} + + + + + + Access Denied + + + + Unable to access file information. + + + - - {{t reCat}} + {{else if @skInventoryApp.appIsNotAvailableOnAppknox}} + + + + + + {{t 'storeknox.appNotInAppknoxHeader'}} + + + + {{t 'storeknox.appNotInAppknoxDescription'}} - - {{this.getVaCategoryResultCount reCat}} + + <:leftIcon> + + + + <:default> + {{t 'appMonitoringModule.initiateUpload'}} + + + + + {{else if @skInventoryApp.coreProjectLatestVersion}} + + + + + {{t 'summary'}} + + + + {{#each this.vaResultsCategories as |resultCat|}} + + + + + + {{t resultCat}} + + + + + {{this.getVaCategoryResultCount resultCat}} + + + {{/each}} + + + + + + + + {{t 'fileID'}} + + + + {{@skInventoryApp.coreProjectLatestVersion.id}} + + + + {{#each this.vaResultsData as |info|}} + + + + + {{info.title}} + + + + {{info.value}} + + + {{/each}} + + + {{/if}} + + +{{! LOADING STATE FOR VA RESULT }} +{{! TODO: Remove if unneeded }} +{{!-- + + <:default> + + {{t 'storeknox.latestVAResults'}} + + + + + + + + + {{t 'summary'}} + + + + + + + + + + {{t 'fileID'}} + + + + + + {{#each this.vaResultsData as |info|}} + + + + + {{info.title}} + + + + + {{/each}} + + + --}} + +{{! UPLOAD COMPLETE STATE - INITIATE UPLOAD }} +{{!-- + + <:default> + + {{t 'storeknox.latestVAResults'}} + + + + + + + + + Completed... + 100% - - {{/each}} - \ No newline at end of file + + + + --}} + +{{! UPLOAD IN PROGRESS STATE - INITIATE UPLOAD }} +{{!-- + + <:default> + + {{t 'storeknox.latestVAResults'}} + + + + + + + + + Processing... + 70% + + + + + + --}} + +{{! INSUFFICIENT CREDITS - INITIATE UPLOAD }} +{{!-- + + <:default> + + {{t 'storeknox.latestVAResults'}} + + + + + + + + + + Insufficient Credits + + + + We tried to upload, due to Insufficient Credits. The upload has been + cancelled. + + + + + Contact Support + + + --}} \ No newline at end of file diff --git a/app/components/storeknox/inventory-details/app-details/va-results/index.ts b/app/components/storeknox/inventory-details/app-details/va-results/index.ts index fd146b847..dc661ed55 100644 --- a/app/components/storeknox/inventory-details/app-details/va-results/index.ts +++ b/app/components/storeknox/inventory-details/app-details/va-results/index.ts @@ -1,18 +1,40 @@ import { action } from '@ember/object'; +import { service } from '@ember/service'; +import { task } from 'ember-concurrency'; import Component from '@glimmer/component'; +import dayjs from 'dayjs'; +import { tracked } from 'tracked-built-ins'; +import type IntlService from 'ember-intl/services/intl'; +import type Store from '@ember-data/store'; + import type SkInventoryAppModel from 'irene/models/sk-inventory-app'; +import type MeService from 'irene/services/me'; +import type FileModel from 'irene/models/file'; interface StoreknoxInventoryDetailsAppDetailsVaResultsSignature { Args: { - app?: SkInventoryAppModel; + skInventoryApp?: SkInventoryAppModel; }; } export default class StoreknoxInventoryDetailsAppDetailsVaResultsComponent extends Component { - get coreProjectLatestVersion() { - return this.args.app?.coreProjectLatestVersion; + @service declare intl: IntlService; + @service declare me: MeService; + @service declare store: Store; + + @tracked coreProjectLatestVersion: FileModel | null = null; + @tracked fileNotAccessibleToUser = false; + + constructor( + owner: unknown, + args: StoreknoxInventoryDetailsAppDetailsVaResultsSignature['Args'] + ) { + super(owner, args); + + this.fetchCoreProjectLatestVersion.perform(); } + // Latest file related to project on Appknox get vaResults() { return { critical: this.coreProjectLatestVersion?.get('countRiskCritical'), @@ -25,14 +47,45 @@ export default class StoreknoxInventoryDetailsAppDetailsVaResultsComponent exten } get vaResultsCategories() { - return Object.keys(this.vaResults) as Array< - 'medium' | 'critical' | 'high' | 'low' | 'passed' | 'untested' - >; + return Object.keys(this.vaResults) as Array; + } + + get vaResultsData() { + return [ + { + title: this.intl.t('version'), + value: this.coreProjectLatestVersion?.get('version'), + }, + { + title: this.intl.t('versionCodeTitleCase'), + value: this.coreProjectLatestVersion?.get('versionCode'), + }, + { + title: this.intl.t('storeknox.lastScannedDate'), + value: dayjs(this.coreProjectLatestVersion?.get('createdOn')).format( + 'DD MMM YYYY' + ), + }, + ]; } @action getVaCategoryResultCount(category: keyof typeof this.vaResults) { return this.vaResults[category]; } + + fetchCoreProjectLatestVersion = task(async () => { + const id = this.args.skInventoryApp?.coreProjectLatestVersion?.id as string; + + try { + this.coreProjectLatestVersion = await this.store.findRecord('file', id); + } catch (err) { + const error = err as AdapterError; + + if (error?.errors?.[0]?.status === '404') { + this.fileNotAccessibleToUser = true; + } + } + }); } declare module '@glint/environment-ember-loose/registry' { diff --git a/app/components/storeknox/inventory-details/brand-abuse/index.hbs b/app/components/storeknox/inventory-details/brand-abuse/index.hbs index 02661e76d..5341c7853 100644 --- a/app/components/storeknox/inventory-details/brand-abuse/index.hbs +++ b/app/components/storeknox/inventory-details/brand-abuse/index.hbs @@ -1,9 +1,11 @@ <:illustration> - + diff --git a/app/components/storeknox/inventory-details/feature-unavailable/index.hbs b/app/components/storeknox/inventory-details/feature-unavailable/index.hbs index 6cf1951dd..118d98e80 100644 --- a/app/components/storeknox/inventory-details/feature-unavailable/index.hbs +++ b/app/components/storeknox/inventory-details/feature-unavailable/index.hbs @@ -5,6 +5,7 @@ @width='full' local-class='feature-unavailable-root' {{style padding='4.2857em 1.5em'}} + data-test-storeknoxInventoryDetails-featureUnavailable-root > {{yield to='illustration'}} @@ -13,12 +14,16 @@ @alignItems='center' @spacing='0.5' {{style maxWidth='485px'}} + data-test-storeknoxInventoryDetails-featureUnavailable-headerTitle > {{@headerTitle}} - + {{@headerDescription}} diff --git a/app/components/storeknox/inventory-details/header/index.hbs b/app/components/storeknox/inventory-details/header/index.hbs index 5c45dd3e7..f41aa0a18 100644 --- a/app/components/storeknox/inventory-details/header/index.hbs +++ b/app/components/storeknox/inventory-details/header/index.hbs @@ -1,7 +1,7 @@
@@ -9,11 +9,18 @@ {{#if this.skInventoryApp.appIsNotAvailableOnAppknox}} <:icon> - + <:default> - + {{t 'storeknox.appNotPartOfAppknox'}} @@ -29,6 +36,7 @@ local-class='app-info-logo' src={{@skInventoryApp.appMetadata.iconUrl}} alt={{@skInventoryApp.appMetadata.title}} + data-test-storeknoxInventoryDetails-appLogoImage /> {{@skInventoryApp.appMetadata.title}} - + {{@skInventoryApp.appMetadata.packageName}} @@ -48,6 +61,7 @@ @@ -56,15 +70,22 @@ @storeLink={{@skInventoryApp.appMetadata.url}} > {{#if this.skInventoryApp.isAndroid}} - + + {{else}} - + {{/if}} {{#unless this.skInventoryApp.appIsNotAvailableOnAppknox}} - + {{/unless}} @@ -89,6 +110,7 @@ @label={{this.activeRouteTagProps.label}} @hideRightIcon={{true}} {{style height='32px' border='none'}} + data-test-storeknoxInventoryDetails-pageInfoTag /> {{else}} @@ -106,11 +128,15 @@ this.toggleSkInventoryAppMonitoring.isRunning }} {{style margin='0em'}} + data-test-storeknoxInventoryDetails-monitoringStatusToggle /> - - Monitoring Action + + {{t 'storeknox.monitoringAction'}} {{/if}} diff --git a/app/components/storeknox/inventory-details/header/index.ts b/app/components/storeknox/inventory-details/header/index.ts index a6637c59a..531d42f78 100644 --- a/app/components/storeknox/inventory-details/header/index.ts +++ b/app/components/storeknox/inventory-details/header/index.ts @@ -6,7 +6,6 @@ import type IntlService from 'ember-intl/services/intl'; import type RouterService from '@ember/routing/router-service'; import type Store from '@ember-data/store'; -import ENUMS from 'irene/enums'; import parseError from 'irene/utils/parse-error'; import type SkInventoryAppModel from 'irene/models/sk-inventory-app'; import type OrganizationService from 'irene/services/organization'; @@ -71,18 +70,15 @@ export default class StoreknoxInventoryDetailsHeaderComponent extends Component< get routeTagList() { const skInventoryAppId = this.skInventoryApp?.id as string; - const appCoreProjectLatestVersionId = this.appCoreProjectLatestVersion?.id; return [ { id: 'unscanned-version', disabled: false, label: this.intl.t('storeknox.unscannedVersion'), - needsAction: - this.skInventoryApp?.storeMonitoringStatus === - ENUMS.SK_APP_MONITORING_STATUS.UNSCANNED, + needsAction: this.skInventoryApp?.containsUnscannedVersion, route: 'authenticated.storeknox.inventory-details.unscanned-version', - models: [skInventoryAppId, appCoreProjectLatestVersionId as string], + models: [skInventoryAppId], }, { id: 'brand-abuse', @@ -115,18 +111,15 @@ export default class StoreknoxInventoryDetailsHeaderComponent extends Component< toggleSkInventoryAppMonitoring = task(async (checked: boolean) => { try { - this.skInventoryApp?.set('monitoringEnabled', checked); - await this.skInventoryApp?.toggleMonitoring(checked); await this.skInventoryApp?.reload(); this.notify.success( - 'Monitoring ' + `${checked ? 'Enabled' : 'Disabled'}` + this.intl.t('storeknox.monitoring') + + ` ${checked ? this.intl.t('enabled') : this.intl.t('disabled')}` ); } catch (error) { this.notify.error(parseError(error)); - - this.skInventoryApp?.rollbackAttributes(); } }); } diff --git a/app/components/storeknox/inventory-details/malware-detected/index.hbs b/app/components/storeknox/inventory-details/malware-detected/index.hbs index af450c3db..c6b1292ce 100644 --- a/app/components/storeknox/inventory-details/malware-detected/index.hbs +++ b/app/components/storeknox/inventory-details/malware-detected/index.hbs @@ -1,9 +1,11 @@ <:illustration> - + diff --git a/app/components/storeknox/inventory-details/section-header/index.scss b/app/components/storeknox/inventory-details/section-header/index.scss index 7a18de81a..ea17620ea 100644 --- a/app/components/storeknox/inventory-details/section-header/index.scss +++ b/app/components/storeknox/inventory-details/section-header/index.scss @@ -1,9 +1,10 @@ .storeknox-inventory-details-section-header-root { height: 40px; - border-bottom: 0px; border: 1px solid var(--storeknox-inventory-details-section-header-root-border-color); + border-bottom: 0px; + &.default { background: var( --storeknox-inventory-details-section-header-default-bg-color diff --git a/app/components/storeknox/inventory/app-list/index.hbs b/app/components/storeknox/inventory/app-list/index.hbs index 8194613a0..ba37b5244 100644 --- a/app/components/storeknox/inventory/app-list/index.hbs +++ b/app/components/storeknox/inventory/app-list/index.hbs @@ -19,5 +19,5 @@ --}}
- +
\ No newline at end of file diff --git a/app/components/storeknox/inventory/app-list/index.ts b/app/components/storeknox/inventory/app-list/index.ts index 626e384c0..4f3cd2e1d 100644 --- a/app/components/storeknox/inventory/app-list/index.ts +++ b/app/components/storeknox/inventory/app-list/index.ts @@ -2,15 +2,7 @@ import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; -import { type StoreknoxInventoryAppListQueryParams } from 'irene/routes/authenticated/storeknox/inventory/app-list'; - -export interface StoreknoxInventoryAppListSignature { - Args: { - queryParams: StoreknoxInventoryAppListQueryParams; - }; -} - -export default class StoreknoxInventoryAppListComponent extends Component { +export default class StoreknoxInventoryAppListComponent extends Component { @tracked searchQuery = ''; @tracked discoverClicked = false; diff --git a/app/components/storeknox/inventory/app-list/table/index.hbs b/app/components/storeknox/inventory/app-list/table/index.hbs index b6b2d0867..8e92b61f3 100644 --- a/app/components/storeknox/inventory/app-list/table/index.hbs +++ b/app/components/storeknox/inventory/app-list/table/index.hbs @@ -1,69 +1,101 @@ - - - - - - {{#if column.headerComponent}} - {{#let (component column.headerComponent) as |Component|}} - - {{/let}} - {{else}} - {{column.name}} - {{/if}} - - - +{{#if this.hasNoApps}} + + - - - - {{#let (component r.columnValue.cellComponent) as |Component|}} - - {{/let}} - - - - + + {{t 'storeknox.noRequestedAppsFound'}} + - {{#unless this.isFetchingTableData}} - - {{/unless}} - \ No newline at end of file + + {{t 'storeknox.noRequestedAppsFoundDescription' htmlSafe=true}} + + + +{{else}} + + + + + + {{#if column.headerComponent}} + {{#let (component column.headerComponent) as |Component|}} + + {{/let}} + {{else}} + {{column.name}} + {{/if}} + + + + + + + + {{#let (component r.columnValue.cellComponent) as |Component|}} + + {{/let}} + + + + + + {{#unless this.isFetchingTableData}} + + {{/unless}} + +{{/if}} \ No newline at end of file diff --git a/app/components/storeknox/inventory/app-list/table/index.scss b/app/components/storeknox/inventory/app-list/table/index.scss new file mode 100644 index 000000000..1ff4af5ee --- /dev/null +++ b/app/components/storeknox/inventory/app-list/table/index.scss @@ -0,0 +1,17 @@ +.empty-container { + padding-top: 5em; + padding-bottom: 5em; + border: 1px solid + var(--storeknox-discover-requested-apps-table-row-border-color); + background-color: var(--storeknox-discover-requested-apps-table-row-color); + border-top: 0; + + .header { + margin-top: 1.5625em; + } + + .body-text { + text-align: center; + max-width: 550px; + } +} diff --git a/app/components/storeknox/inventory/app-list/table/index.ts b/app/components/storeknox/inventory/app-list/table/index.ts index 7ffa76c3e..68e031340 100644 --- a/app/components/storeknox/inventory/app-list/table/index.ts +++ b/app/components/storeknox/inventory/app-list/table/index.ts @@ -1,27 +1,17 @@ -// eslint-disable-next-line ember/use-ember-data-rfc-395-imports -import DS from 'ember-data'; import Component from '@glimmer/component'; import { action } from '@ember/object'; import { service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; -import { task } from 'ember-concurrency'; import type RouterService from '@ember/routing/router-service'; -import type { Owner } from '@ember/test-helpers/build-owner'; +import type IntlService from 'ember-intl/services/intl'; +import type Store from '@ember-data/store'; -import parseError from 'irene/utils/parse-error'; import ENUMS from 'irene/enums'; -import type IntlService from 'ember-intl/services/intl'; import type MeService from 'irene/services/me'; -import type Store from '@ember-data/store'; -import type SkInventoryAppModel from 'irene/models/sk-inventory-app'; import type { StoreknoxCommonTableColumnsData } from 'irene/components/storeknox/table-columns'; import type { PaginationProviderActionsArgs } from 'irene/components/ak-pagination-provider'; -import type { StoreknoxInventoryAppListQueryParams } from 'irene/routes/authenticated/storeknox/inventory/app-list'; - -type SkAppsQueryResponse = - DS.AdapterPopulatedRecordArray & { - meta: { count: number }; - }; +import type SkInventoryAppModel from 'irene/models/sk-inventory-app'; +import type SkInventoryAppService from 'irene/services/sk-inventory-apps'; type StoreknoxInventoryTableDataItem = StoreknoxCommonTableColumnsData & { appIsSelected: boolean; @@ -33,7 +23,6 @@ export interface StoreknoxInventoryAppListTableSignature { Args: { isAddingAppToInventory?: boolean; loadDisabledApps?: boolean; - queryParams: StoreknoxInventoryAppListQueryParams; handleSelectedDisabledApps?: (apps: string[]) => void; }; } @@ -45,27 +34,18 @@ export default class StoreknoxInventoryAppListTableComponent extends Component(); @tracked selectedDisabledAppIds: string[] = []; - constructor( - owner: Owner, - args: StoreknoxInventoryAppListTableSignature['Args'] - ) { - super(owner, args); - - const { app_limit, app_offset } = args.queryParams; - - this.fetchSkInventoryApps.perform(app_limit, app_offset, false); - } - get loadDisabledApps() { return this.args.loadDisabledApps; } get disableRowClick() { - return this.loadDisabledApps || this.fetchSkInventoryApps.isRunning; + return this.loadDisabledApps || this.isFetchingTableData; } get columns() { @@ -110,7 +90,11 @@ export default class StoreknoxInventoryAppListTableComponent extends Component { + return (this.skInventoryAppsService.skInventoryApps?.map((app) => { const { appMetadata } = app; return { @@ -145,7 +129,7 @@ export default class StoreknoxInventoryAppListTableComponent extends Component { - if (updateQueryParams) { - this.updateRouteParams(limit, offset); - } - - const query = !this.loadDisabledApps - ? { - approval_status: ENUMS.SK_APPROVAL_STATUS.APPROVED, - app_status: ENUMS.SK_APP_STATUS.ACTIVE, - } - : { - app_status: ENUMS.SK_APP_STATUS.INACTIVE, - }; - - try { - const data = (await this.store.query('sk-app', { - limit: limit, - offset: offset, - ...query, - })) as SkAppsQueryResponse; - - this.skAppsResponse = data; - } catch (error) { - this.notify.error(parseError(error, this.intl.t('somethingWentWrong'))); - } - } - ); } declare module '@glint/environment-ember-loose/registry' { diff --git a/app/components/storeknox/inventory/app-list/table/monitoring-status/index.hbs b/app/components/storeknox/inventory/app-list/table/monitoring-status/index.hbs index e790d86f7..89f2e3696 100644 --- a/app/components/storeknox/inventory/app-list/table/monitoring-status/index.hbs +++ b/app/components/storeknox/inventory/app-list/table/monitoring-status/index.hbs @@ -6,16 +6,18 @@ > <:tooltipContent>
- - - {{t 'reason'}} - - + + {{t 'reason'}} + - + {{this.tooltipMessage}} {{t 'storeknox.haveBeenDetected'}} @@ -33,9 +35,12 @@ @iconName={{this.monitoringStatusIconDetails.name}} @color={{this.monitoringStatusIconDetails.color}} @size='small' + data-test-storeknoxInventory-appListTable-monitoringStatusIcon /> - + {{#if this.appIsDisabled}} {{t 'disabled'}} {{else if this.needsAction}} diff --git a/app/components/storeknox/inventory/index.hbs b/app/components/storeknox/inventory/index.hbs index 963b3b6f1..bf2f96f87 100644 --- a/app/components/storeknox/inventory/index.hbs +++ b/app/components/storeknox/inventory/index.hbs @@ -2,14 +2,21 @@ @direction='row' @alignItems='center' @justifyContent='space-between' - local-class='header-storeknox-discover-page' + local-class='header-storeknox-inventory-page' > - + {{t 'storeknox.inventoryHeader'}} - + {{t 'storeknox.inventoryDescription'}} @@ -20,6 +27,7 @@ @underline='none' @color='inherit' @route='authenticated.storeknox.discover.result' + data-test-storeknoxInventory-discoveryPageLink > {{t 'storeknox.discoverHeader'}} @@ -37,6 +45,7 @@ @size='medium' @variant='outlined' {{on 'click' this.openSettingsDrawer}} + data-test-storeknoxInventory-settingsDrawerTrigger > @@ -52,6 +61,7 @@ @route={{item.route}} @hasBadge={{item.hasBadge}} @badgeCount={{item.badgeCount}} + data-test-storeknoxInventory-tabs='{{item.id}}-tab' > {{item.label}} @@ -75,7 +85,7 @@ @color='inherit' @variant='h5' @fontWeight='medium' - data-test-addProjectTeam-drawerContainer-title + data-test-storeknoxInventory-settingsDrawer-title > {{t 'storeknox.inventorySettings'}} @@ -83,7 +93,7 @@ @@ -96,11 +106,18 @@ class='px-3 py-4' > - + {{t 'storeknox.addAppknoxProjectsByDefault'}} - + {{t 'storeknox.addAppknoxProjectsByDefaultToggleDescription'}} @@ -109,6 +126,7 @@ @size='small' @checked={{this.selectedSkOrg.addAppknoxProjectToInventoryByDefault}} @onChange={{this.onToggleAddToInventoryByDefault}} + data-test-storeknoxInventory-settingsDrawer-addAppknoxProjectsByDefaultToggle /> diff --git a/app/components/storeknox/inventory/index.scss b/app/components/storeknox/inventory/index.scss index 2301f9b45..df10e188d 100644 --- a/app/components/storeknox/inventory/index.scss +++ b/app/components/storeknox/inventory/index.scss @@ -1,4 +1,4 @@ -.header-storeknox-discover-page { +.header-storeknox-inventory-page { background-color: white; margin: 0.714em 0; border: 1px solid var(--storeknox-discover-header-border-color); diff --git a/app/components/storeknox/inventory/index.ts b/app/components/storeknox/inventory/index.ts index 68e359b00..d81b1a7f8 100644 --- a/app/components/storeknox/inventory/index.ts +++ b/app/components/storeknox/inventory/index.ts @@ -1,5 +1,3 @@ -// eslint-disable-next-line ember/use-ember-data-rfc-395-imports -import DS from 'ember-data'; import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; import { action } from '@ember/object'; @@ -8,17 +6,11 @@ import { task } from 'ember-concurrency'; import type Store from '@ember-data/store'; import type IntlService from 'ember-intl/services/intl'; -import ENUMS from 'irene/enums'; import parseError from 'irene/utils/parse-error'; import type SkPendingReviewService from 'irene/services/sk-pending-review'; import type MeService from 'irene/services/me'; -import type SkInventoryAppModel from 'irene/models/sk-inventory-app'; import type SkOrganizationModel from 'irene/models/sk-organization'; - -type SkAppsQueryResponse = - DS.AdapterPopulatedRecordArray & { - meta: { count: number }; - }; +import type SkInventoryAppService from 'irene/services/sk-inventory-apps'; export default class StoreknoxInventoryComponent extends Component { @service declare intl: IntlService; @@ -27,22 +19,23 @@ export default class StoreknoxInventoryComponent extends Component { @service declare skPendingReview: SkPendingReviewService; @service('notifications') declare notify: NotificationService; + @service('sk-inventory-apps') + declare skInventoryAppsService: SkInventoryAppService; + @tracked selectedSkOrg: SkOrganizationModel | undefined; @tracked showWelcomeModal = false; @tracked showSettingsDrawer = false; - @tracked totalInventoryAppsCount = 0; @tracked totalDisabledAppsCount = 0; constructor(owner: unknown, args: object) { super(owner, args); - this.getTabItemsCount.perform(); this.getSkOrganization.perform(); } get isOwner() { - return this.me.org?.is_owner; + return !!this.me.org?.is_owner; } get tabItems() { @@ -51,15 +44,15 @@ export default class StoreknoxInventoryComponent extends Component { id: 'app-inventory', route: 'authenticated.storeknox.inventory.app-list', label: this.intl.t('storeknox.appInventory'), - hasBadge: this.totalInventoryAppsCount > 0, - badgeCount: this.totalInventoryAppsCount, + hasBadge: this.skInventoryAppsService.skInventoryAppsCount > 0, + badgeCount: this.skInventoryAppsService.skInventoryAppsCount, }, this.isOwner && { id: 'pending-review', route: 'authenticated.storeknox.inventory.pending-reviews', label: this.intl.t('storeknox.pendingReview'), - hasBadge: this.skPendingReview.totalCount > 0, - badgeCount: this.skPendingReview.totalCount, + hasBadge: this.skPendingReview.skPendingReviewAppsCount > 0, + badgeCount: this.skPendingReview.skPendingReviewAppsCount, }, // { // id: 'disabled-apps', @@ -88,21 +81,6 @@ export default class StoreknoxInventoryComponent extends Component { this.toggleAddToInventoryByDefault.perform(checked); } - getTabItemsCount = task(async () => { - // Fetches the inventory list items total count - this.totalInventoryAppsCount = ( - (await this.store.query('sk-app', { - approval_status: ENUMS.SK_APPROVAL_STATUS.APPROVED, - app_status: ENUMS.SK_APP_STATUS.ACTIVE, - })) as SkAppsQueryResponse - ).meta.count; - - // Get Pending review data - if (this.isOwner) { - this.skPendingReview.fetchPendingReviewApps.perform(10, 0, false); - } - }); - toggleAddToInventoryByDefault = task(async (checked?: boolean) => { try { const org = diff --git a/app/components/storeknox/inventory/pending-review/empty/index.hbs b/app/components/storeknox/inventory/pending-review/empty/index.hbs index c2be72cf3..b019e431e 100644 --- a/app/components/storeknox/inventory/pending-review/empty/index.hbs +++ b/app/components/storeknox/inventory/pending-review/empty/index.hbs @@ -1,11 +1,21 @@ - + - + {{t 'storeknox.noPendingItems'}} - + {{t 'storeknox.noPendingItemsDescription'}} \ No newline at end of file diff --git a/app/components/storeknox/inventory/pending-review/index.hbs b/app/components/storeknox/inventory/pending-review/index.hbs index 0361298cc..ff6c20e14 100644 --- a/app/components/storeknox/inventory/pending-review/index.hbs +++ b/app/components/storeknox/inventory/pending-review/index.hbs @@ -50,15 +50,13 @@ {{#if this.showTable}} {{else}} diff --git a/app/components/storeknox/inventory/pending-review/index.ts b/app/components/storeknox/inventory/pending-review/index.ts index f28c2aec4..3c84ba63e 100644 --- a/app/components/storeknox/inventory/pending-review/index.ts +++ b/app/components/storeknox/inventory/pending-review/index.ts @@ -2,17 +2,11 @@ import Component from '@glimmer/component'; import { action } from '@ember/object'; import { inject as service } from '@ember/service'; import type IntlService from 'ember-intl/services/intl'; +import type RouterService from '@ember/routing/router-service'; -import type { StoreknoxInventoryPendingReviewsQueryParam } from 'irene/routes/authenticated/storeknox/inventory/pending-reviews'; import type SkPendingReviewService from 'irene/services/sk-pending-review'; import type SkAppModel from 'irene/models/sk-app'; -export interface StoreknoxInventoryResultsSignature { - Args: { - queryParams: StoreknoxInventoryPendingReviewsQueryParam; - }; -} - interface LimitOffset { limit: number; offset: number; @@ -20,60 +14,9 @@ interface LimitOffset { export default class StoreknoxInventoryPendingReviewComponent extends Component { @service declare intl: IntlService; + @service declare router: RouterService; @service declare skPendingReview: SkPendingReviewService; - constructor( - owner: unknown, - args: StoreknoxInventoryResultsSignature['Args'] - ) { - super(owner, args); - - const { app_limit, app_offset } = args.queryParams; - - this.skPendingReview.fetchPendingReviewApps.perform(app_limit, app_offset); - } - - get columns() { - return [ - // { - // headerComponent: 'storeknox/table-columns/checkbox-header', - // cellComponent: 'storeknox/table-columns/checkbox', - // minWidth: 10, - // width: 10, - // textAlign: 'center', - // }, - { - headerComponent: 'storeknox/table-columns/store-header', - cellComponent: 'storeknox/table-columns/store', - minWidth: 50, - width: 50, - textAlign: 'center', - }, - { - name: this.intl.t('application'), - cellComponent: 'storeknox/table-columns/application', - width: 200, - }, - { - headerComponent: - 'storeknox/inventory/pending-review/table/found-by-header', - cellComponent: 'storeknox/inventory/pending-review/table/found-by', - }, - // { - // headerComponent: - // 'storeknox/inventory/pending-review/table/availability-header', - // cellComponent: 'storeknox/inventory/pending-review/table/availability', - // textAlign: 'center', - // }, - { - name: this.intl.t('status'), - cellComponent: 'storeknox/inventory/pending-review/table/status', - textAlign: 'center', - width: 80, - }, - ]; - } - get reviewAppsData() { if (this.isFetchingData) { return Array.from({ length: 5 }, () => ({})) as SkAppModel[]; @@ -83,15 +26,23 @@ export default class StoreknoxInventoryPendingReviewComponent extends Component< } // Table Actions - @action goToPage(args: LimitOffset) { - this.skPendingReview.fetchPendingReviewApps.perform( - args.limit, - args.offset - ); + @action goToPage({ limit, offset }: LimitOffset) { + // Route model reloads and fetches apps in service + this.router.transitionTo({ + queryParams: { + app_limit: limit, + app_offset: offset, + }, + }); } @action changePerPageItem(args: LimitOffset) { - this.skPendingReview.fetchPendingReviewApps.perform(args.limit, 0); + this.router.transitionTo({ + queryParams: { + app_limit: args.limit, + app_offset: 0, + }, + }); } // TO DO: When Multiple Scenario comes in @@ -105,13 +56,13 @@ export default class StoreknoxInventoryPendingReviewComponent extends Component< get isFetchingData() { return ( - this.skPendingReview.fetchPendingReviewApps.isRunning && + this.skPendingReview.isFetchingSkPendingReviewApps && !this.skPendingReview.singleUpdate ); } get totalCount() { - return this.skPendingReview.totalCount; + return this.skPendingReview.skPendingReviewAppsCount; } } diff --git a/app/components/storeknox/inventory/pending-review/table/index.hbs b/app/components/storeknox/inventory/pending-review/table/index.hbs index 5917bd83f..8034ed166 100644 --- a/app/components/storeknox/inventory/pending-review/table/index.hbs +++ b/app/components/storeknox/inventory/pending-review/table/index.hbs @@ -4,13 +4,13 @@ @totalItems={{@totalCount}} @nextAction={{@goToPage}} @prevAction={{@goToPage}} - @itemPerPageOptions={{this.itemPerPageOptions}} + @itemPerPageOptions={{array 10 25 50}} @defaultLimit={{@limit}} @offset={{@offset}} as |pgc| > - + {{#if column.headerComponent}} @@ -24,14 +24,14 @@ - + {{#let (component r.columnValue.cellComponent) as |Component|}} - + {{/let}} diff --git a/app/components/storeknox/inventory/pending-review/table/index.ts b/app/components/storeknox/inventory/pending-review/table/index.ts index cd19e3742..7c2f57075 100644 --- a/app/components/storeknox/inventory/pending-review/table/index.ts +++ b/app/components/storeknox/inventory/pending-review/table/index.ts @@ -1,6 +1,7 @@ import Component from '@glimmer/component'; +import { service } from '@ember/service'; +import type IntlService from 'ember-intl/services/intl'; -import type { StoreknoxInventoryPendingReviewsQueryParam } from 'irene/routes/authenticated/storeknox/inventory/pending-reviews'; import type SkAppModel from 'irene/models/sk-app'; interface LimitOffset { @@ -17,13 +18,51 @@ export interface StoreknoxInventoryPendingReviewTableSignature { totalCount: number; goToPage: (args: LimitOffset) => void; onItemPerPageChange: (args: LimitOffset) => void; - queryParams: StoreknoxInventoryPendingReviewsQueryParam; }; } export default class StoreknoxInventoryPendingReviewTableComponent extends Component { - get itemPerPageOptions() { - return [10, 25, 50]; + @service declare intl: IntlService; + + get columns() { + return [ + // { + // headerComponent: 'storeknox/table-columns/checkbox-header', + // cellComponent: 'storeknox/table-columns/checkbox', + // minWidth: 10, + // width: 10, + // textAlign: 'center', + // }, + { + headerComponent: 'storeknox/table-columns/store-header', + cellComponent: 'storeknox/table-columns/store', + minWidth: 50, + width: 50, + textAlign: 'center', + }, + { + name: this.intl.t('application'), + cellComponent: 'storeknox/table-columns/application', + width: 200, + }, + { + headerComponent: + 'storeknox/inventory/pending-review/table/requested-by-header', + cellComponent: 'storeknox/inventory/pending-review/table/requested-by', + }, + // { + // headerComponent: + // 'storeknox/inventory/pending-review/table/availability-header', + // cellComponent: 'storeknox/inventory/pending-review/table/availability', + // textAlign: 'center', + // }, + { + name: this.intl.t('status'), + cellComponent: 'storeknox/inventory/pending-review/table/status', + textAlign: 'center', + width: 80, + }, + ]; } get showPagination() { diff --git a/app/components/storeknox/inventory/pending-review/table/found-by-header/index.hbs b/app/components/storeknox/inventory/pending-review/table/requested-by-header/index.hbs similarity index 100% rename from app/components/storeknox/inventory/pending-review/table/found-by-header/index.hbs rename to app/components/storeknox/inventory/pending-review/table/requested-by-header/index.hbs diff --git a/app/components/storeknox/inventory/pending-review/table/found-by-header/index.scss b/app/components/storeknox/inventory/pending-review/table/requested-by-header/index.scss similarity index 100% rename from app/components/storeknox/inventory/pending-review/table/found-by-header/index.scss rename to app/components/storeknox/inventory/pending-review/table/requested-by-header/index.scss diff --git a/app/components/storeknox/inventory/pending-review/table/found-by-header/index.ts b/app/components/storeknox/inventory/pending-review/table/requested-by-header/index.ts similarity index 84% rename from app/components/storeknox/inventory/pending-review/table/found-by-header/index.ts rename to app/components/storeknox/inventory/pending-review/table/requested-by-header/index.ts index baebb11d8..41e7147f5 100644 --- a/app/components/storeknox/inventory/pending-review/table/found-by-header/index.ts +++ b/app/components/storeknox/inventory/pending-review/table/requested-by-header/index.ts @@ -6,7 +6,7 @@ import type IntlService from 'ember-intl/services/intl'; import ENUMS from 'irene/enums'; -export default class StoreknoxInventoryPendingReviewTableFoundByHeaderComponent extends Component { +export default class StoreknoxInventoryPendingReviewTableRequestedByHeaderComponent extends Component { @service declare intl: IntlService; @tracked anchorRef: HTMLElement | null = null; @@ -61,6 +61,6 @@ export default class StoreknoxInventoryPendingReviewTableFoundByHeaderComponent declare module '@glint/environment-ember-loose/registry' { export default interface Registry { - 'Storeknox::Inventory::PendingReview::Table::FoundByHeader': typeof StoreknoxInventoryPendingReviewTableFoundByHeaderComponent; + 'Storeknox::Inventory::PendingReview::Table::RequestedByHeader': typeof StoreknoxInventoryPendingReviewTableRequestedByHeaderComponent; } } diff --git a/app/components/storeknox/inventory/pending-review/table/found-by/index.hbs b/app/components/storeknox/inventory/pending-review/table/requested-by/index.hbs similarity index 58% rename from app/components/storeknox/inventory/pending-review/table/found-by/index.hbs rename to app/components/storeknox/inventory/pending-review/table/requested-by/index.hbs index 98ed1c632..2b12673b5 100644 --- a/app/components/storeknox/inventory/pending-review/table/found-by/index.hbs +++ b/app/components/storeknox/inventory/pending-review/table/requested-by/index.hbs @@ -3,11 +3,17 @@ {{else}} {{#if this.isManual}} - + {{@data.addedBy.email}} {{else}} - + {{t 'system'}} {{/if}} @@ -19,11 +25,15 @@ @color='inherit' @fontWeight='medium' local-class='section-header' + data-test-storeknoxInventory-pendingReviewTable-requestedOnHeaderText > {{t 'requestedOn'}} - + {{this.addedOn}} @@ -35,11 +45,15 @@ @color='inherit' @fontWeight='medium' local-class='section-header' + data-test-storeknoxInventory-pendingReviewTable-foundByHeader > {{t 'type'}} - + {{this.foundBy}} @@ -47,7 +61,11 @@ <:default> - + diff --git a/app/components/storeknox/inventory/pending-review/table/found-by/index.scss b/app/components/storeknox/inventory/pending-review/table/requested-by/index.scss similarity index 100% rename from app/components/storeknox/inventory/pending-review/table/found-by/index.scss rename to app/components/storeknox/inventory/pending-review/table/requested-by/index.scss diff --git a/app/components/storeknox/inventory/pending-review/table/found-by/index.ts b/app/components/storeknox/inventory/pending-review/table/requested-by/index.ts similarity index 58% rename from app/components/storeknox/inventory/pending-review/table/found-by/index.ts rename to app/components/storeknox/inventory/pending-review/table/requested-by/index.ts index 88325c781..7230244b1 100644 --- a/app/components/storeknox/inventory/pending-review/table/found-by/index.ts +++ b/app/components/storeknox/inventory/pending-review/table/requested-by/index.ts @@ -1,29 +1,31 @@ import Component from '@glimmer/component'; import dayjs from 'dayjs'; -import type IntlService from 'ember-intl/services/intl'; import { inject as service } from '@ember/service'; +import type IntlService from 'ember-intl/services/intl'; -import type SkAppModel from 'irene/models/sk-app'; import ENUMS from 'irene/enums'; +import type SkAppModel from 'irene/models/sk-app'; -interface StoreknoxInventoryPendingReviewTableFoundBySignature { +interface StoreknoxInventoryPendingReviewTableRequestedBySignature { Args: { data: SkAppModel; loading: boolean; }; } -export default class StoreknoxInventoryPendingReviewTableFoundByComponent extends Component { +export default class StoreknoxInventoryPendingReviewTableRequestedByComponent extends Component { @service declare intl: IntlService; get addedOn() { return dayjs(this.args.data.addedOn).format('MMMM D, YYYY, HH:mm'); } - get foundBy() { - const appSource = this.args.data.appSource; + get appSource() { + return this.args.data.appSource; + } - if (appSource === ENUMS.SK_DISCOVERY.MANUAL) { + get foundBy() { + if (this.appSource === ENUMS.SK_DISCOVERY.MANUAL) { return this.intl.t('storeknox.manualDiscovery'); } else { return this.intl.t('storeknox.autoDiscovery'); @@ -31,14 +33,12 @@ export default class StoreknoxInventoryPendingReviewTableFoundByComponent extend } get isManual() { - const appSource = this.args.data.appSource; - - return appSource === ENUMS.SK_DISCOVERY.MANUAL; + return this.appSource === ENUMS.SK_DISCOVERY.MANUAL; } } declare module '@glint/environment-ember-loose/registry' { export default interface Registry { - 'Storeknox::Inventory::PendingReview::Table::FoundBy': typeof StoreknoxInventoryPendingReviewTableFoundByComponent; + 'Storeknox::Inventory::PendingReview::Table::RequestedBy': typeof StoreknoxInventoryPendingReviewTableRequestedByComponent; } } diff --git a/app/components/storeknox/inventory/pending-review/table/status/index.hbs b/app/components/storeknox/inventory/pending-review/table/status/index.hbs index 3daa4ef62..a86833bfb 100644 --- a/app/components/storeknox/inventory/pending-review/table/status/index.hbs +++ b/app/components/storeknox/inventory/pending-review/table/status/index.hbs @@ -7,16 +7,34 @@ {{else}} - - + + - - + + {{/if}} -{{else}} + + {{! TODO: Intended for Review Logs Table }} + {{!-- {{else}} {{this.statusDetails.text}} @@ -44,5 +62,5 @@ - + --}} {{/if}} \ No newline at end of file diff --git a/app/components/storeknox/inventory/pending-review/table/status/index.ts b/app/components/storeknox/inventory/pending-review/table/status/index.ts index 99944da98..3426c50bd 100644 --- a/app/components/storeknox/inventory/pending-review/table/status/index.ts +++ b/app/components/storeknox/inventory/pending-review/table/status/index.ts @@ -3,20 +3,18 @@ import { action } from '@ember/object'; import dayjs from 'dayjs'; import { task } from 'ember-concurrency'; import { inject as service } from '@ember/service'; - -import parseError from 'irene/utils/parse-error'; +import { waitForPromise } from '@ember/test-waiters'; import type IntlService from 'ember-intl/services/intl'; +import parseError from 'irene/utils/parse-error'; +import ENUMS from 'irene/enums'; import type SkAppModel from 'irene/models/sk-app'; -import type { StoreknoxInventoryPendingReviewsQueryParam } from 'irene/routes/authenticated/storeknox/inventory/pending-reviews'; import type SkPendingReviewService from 'irene/services/sk-pending-review'; -import ENUMS from 'irene/enums'; interface StoreknoxInventoryPendingReviewTableStatusSignature { Args: { data: SkAppModel; loading: boolean; - queryParams: StoreknoxInventoryPendingReviewsQueryParam; }; } @@ -29,14 +27,6 @@ export default class StoreknoxInventoryPendingReviewTableStatusComponent extends return this.rejectApp.isRunning || this.approveApp.isRunning; } - get approvedOn() { - return dayjs(this.args.data.approvedOn).format('MMMM D, YYYY, HH:mm'); - } - - get rejectedOn() { - return dayjs(this.args.data.rejectedOn).format('MMMM D, YYYY, HH:mm'); - } - get isPending() { return ( this.args.data.approvalStatus === @@ -52,6 +42,16 @@ export default class StoreknoxInventoryPendingReviewTableStatusComponent extends return this.args.data.approvalStatus === ENUMS.SK_APPROVAL_STATUS.REJECTED; } + // TODO: Review when feature is to be worked on + // Intended for Review Logs table + get approvedOn() { + return dayjs(this.args.data.approvedOn).format('MMMM D, YYYY, HH:mm'); + } + + get rejectedOn() { + return dayjs(this.args.data.rejectedOn).format('MMMM D, YYYY, HH:mm'); + } + get statusDetails() { if (this.isApproved) { return { @@ -84,14 +84,9 @@ export default class StoreknoxInventoryPendingReviewTableStatusComponent extends try { const skApp = this.args.data; - await skApp.approveApp(skApp.id); - - const { app_limit, app_offset } = this.args.queryParams; + await waitForPromise(skApp.approveApp(skApp.id)); - this.skPendingReview.fetchPendingReviewApps.perform( - app_limit, - app_offset - ); + this.skPendingReview.reload(); this.notify.success( this.intl.t('storeknox.appAddedToInventory', { @@ -109,14 +104,9 @@ export default class StoreknoxInventoryPendingReviewTableStatusComponent extends try { const skApp = this.args.data; - await skApp.rejectApp(skApp.id); - - const { app_limit, app_offset } = this.args.queryParams; + await waitForPromise(skApp.rejectApp(skApp.id)); - this.skPendingReview.fetchPendingReviewApps.perform( - app_limit, - app_offset - ); + this.skPendingReview.reload(); this.notify.success( this.intl.t('storeknox.appRejected', { diff --git a/app/components/storeknox/product-icon/index.hbs b/app/components/storeknox/product-icon/index.hbs index b19a659ed..f564329d2 100644 --- a/app/components/storeknox/product-icon/index.hbs +++ b/app/components/storeknox/product-icon/index.hbs @@ -1,62 +1,69 @@ -
-
- {{yield}} -
+ {{on 'click' this.toggleProductInfo}} + data-test-storeknox-productIconContainer +> + {{yield}} +
- + - - - - - - INFO - + + + - - {{t 'storeknox.appIsPartOf'}} - {{get this.productTitle @product}} + + {{t 'infoCapitalCase'}} - {{#if this.isAStore}} - - - - {{t 'storeknox.checkOn'}} - {{get this.productTitle @product}} - - - - {{/if}} + + {{t 'storeknox.appIsPartOf'}} + {{get this.productTitle @product}} + - -
\ No newline at end of file + + {{#if this.isAStore}} + + + + {{t 'storeknox.checkOn'}} + {{get this.productTitle @product}} + + + + {{/if}} + + \ No newline at end of file diff --git a/app/components/storeknox/table-columns/application/index.hbs b/app/components/storeknox/table-columns/application/index.hbs index 2b2dcbf8b..9dcefc984 100644 --- a/app/components/storeknox/table-columns/application/index.hbs +++ b/app/components/storeknox/table-columns/application/index.hbs @@ -31,6 +31,7 @@ @underline='always' title={{@data.title}} {{style width='100%'}} + data-test-storeknoxTableColumns-applicationTitle > {{@data.title}}
@@ -41,6 +42,7 @@ @noWrap={{true}} title={{@data.packageName}} {{style width='100%'}} + data-test-storeknoxTableColumns-applicationPackageName > {{@data.packageName}} diff --git a/app/components/storeknox/table-columns/developer/index.hbs b/app/components/storeknox/table-columns/developer/index.hbs index 40e7fdc20..6db5d015a 100644 --- a/app/components/storeknox/table-columns/developer/index.hbs +++ b/app/components/storeknox/table-columns/developer/index.hbs @@ -11,12 +11,18 @@ @noWrap={{true}} title={{@data.devName}} {{style width='100%'}} + data-test-storeknoxTableColumns-applicationDevName > {{@data.devName}} {{#if @data.devEmail}} - + {{@data.devEmail}} {{else}} diff --git a/app/controllers/authenticated/storeknox/inventory/pending-reviews.ts b/app/controllers/authenticated/storeknox/inventory/pending-reviews.ts index 49b6b7936..5f4f35841 100644 --- a/app/controllers/authenticated/storeknox/inventory/pending-reviews.ts +++ b/app/controllers/authenticated/storeknox/inventory/pending-reviews.ts @@ -5,6 +5,7 @@ import type { AkBreadcrumbsItemProps } from 'irene/services/ak-breadcrumbs'; export default class AuthenticatedStoreknoxInventoryPendingReviewsController extends Controller { @service declare intl: IntlService; + queryParams = ['app_limit', 'app_offset', 'app_search_id', 'app_query']; app_limit = 10; diff --git a/app/enums.ts b/app/enums.ts index a2827529a..a1fc32fd2 100644 --- a/app/enums.ts +++ b/app/enums.ts @@ -267,6 +267,13 @@ const ENUMS = { UNSCANNED: 2, NOT_FOUND: 3, }, + + SK_ORGANIZATION_MEMBERSHIP_ROLES: { + MEMBER: 1, + COLLABORATOR: 2, + ADMIN: 3, + OWNER: 4, + }, }; export const ENUMS_DISPLAY = { diff --git a/app/models/sk-app.ts b/app/models/sk-app.ts index 5888e32fa..68fb619e9 100644 --- a/app/models/sk-app.ts +++ b/app/models/sk-app.ts @@ -5,6 +5,7 @@ import type IntlService from 'ember-intl/services/intl'; import ENUMS from 'irene/enums'; import type OrganizationUserModel from './organization-user'; import type SkAppMetadataModel from './sk-app-metadata'; +import SkOrganizationModel from './sk-organization'; export interface AvailabilityData { storeknox: boolean; @@ -41,8 +42,8 @@ export default class SkAppModel extends Model { @attr('number') declare storeMonitoringStatus: number; - @attr('number') - declare skOrganization: number; + @attr('string') + declare storeMonitoringStatusDisplay: string; @attr('date') declare approvedOn: Date; @@ -71,6 +72,9 @@ export default class SkAppModel extends Model { @belongsTo('organization-user', { async: true, inverse: null }) declare rejectedBy: AsyncBelongsTo | string; + @belongsTo('sk-organization', { async: true, inverse: null }) + declare skOrganization: AsyncBelongsTo; + get docUlid() { return this.appMetadata.get('docUlid'); } diff --git a/app/models/sk-inventory-app.ts b/app/models/sk-inventory-app.ts index 14dc76c4c..3b6f72fa1 100644 --- a/app/models/sk-inventory-app.ts +++ b/app/models/sk-inventory-app.ts @@ -1,5 +1,7 @@ import { belongsTo } from '@ember-data/model'; +import ENUMS from 'irene/enums'; import SkAppModel from './sk-app'; + import type ProjectModel from './project'; import type FileModel from './file'; @@ -13,6 +15,12 @@ export default class SkInventoryAppModel extends SkAppModel { get appIsNotAvailableOnAppknox() { return !this.coreProject?.id; } + + get containsUnscannedVersion() { + return ( + this.storeMonitoringStatus === ENUMS.SK_APP_MONITORING_STATUS.UNSCANNED + ); + } } declare module 'ember-data/types/registries/model' { diff --git a/app/routes/authenticated/storeknox/inventory/app-list.ts b/app/routes/authenticated/storeknox/inventory/app-list.ts index 4f1ac2008..30dc6c90b 100644 --- a/app/routes/authenticated/storeknox/inventory/app-list.ts +++ b/app/routes/authenticated/storeknox/inventory/app-list.ts @@ -1,4 +1,8 @@ import AkBreadcrumbsRoute from 'irene/utils/ak-breadcrumbs-route'; +import { service } from '@ember/service'; + +import type SkPendingReviewService from 'irene/services/sk-pending-review'; +import type SkInventoryAppService from 'irene/services/sk-inventory-apps'; export interface StoreknoxInventoryAppListQueryParams { app_limit: number; @@ -6,6 +10,11 @@ export interface StoreknoxInventoryAppListQueryParams { } export default class AuthenticatedStoreknoxInventoryAppListRoute extends AkBreadcrumbsRoute { + @service declare skPendingReview: SkPendingReviewService; + + @service('sk-inventory-apps') + declare skInventoryAppsService: SkInventoryAppService; + queryParams = { app_limit: { refreshModel: true, @@ -15,11 +24,17 @@ export default class AuthenticatedStoreknoxInventoryAppListRoute extends AkBread }, }; - model(params: Partial) { + async model(params: Partial) { const { app_limit, app_offset } = params; - return { - queryParams: { app_limit, app_offset }, - }; + this.skInventoryAppsService + .setLimitOffset({ + limit: app_limit, + offset: app_offset, + }) + .reload(); + + // To get pending review count in tabs when page is visited for the first time + this.skPendingReview.reload(); } } diff --git a/app/routes/authenticated/storeknox/inventory/pending-reviews.ts b/app/routes/authenticated/storeknox/inventory/pending-reviews.ts index 0408e67df..c3f235ee9 100644 --- a/app/routes/authenticated/storeknox/inventory/pending-reviews.ts +++ b/app/routes/authenticated/storeknox/inventory/pending-reviews.ts @@ -1,4 +1,10 @@ +import { service } from '@ember/service'; +import type RouterService from '@ember/routing/router-service'; + import AkBreadcrumbsRoute from 'irene/utils/ak-breadcrumbs-route'; +import type MeService from 'irene/services/me'; +import type SkPendingReviewService from 'irene/services/sk-pending-review'; +import type SkInventoryAppService from 'irene/services/sk-inventory-apps'; export interface StoreknoxInventoryPendingReviewsQueryParam { app_limit: number; @@ -6,6 +12,13 @@ export interface StoreknoxInventoryPendingReviewsQueryParam { } export default class AuthenticatedStoreknoxInventoryPendingReviewsRoute extends AkBreadcrumbsRoute { + @service declare me: MeService; + @service declare router: RouterService; + @service declare skPendingReview: SkPendingReviewService; + + @service('sk-inventory-apps') + declare skInventoryAppsService: SkInventoryAppService; + queryParams = { app_limit: { refreshModel: true, @@ -15,11 +28,23 @@ export default class AuthenticatedStoreknoxInventoryPendingReviewsRoute extends }, }; + async beforeModel() { + if (!this.me.org?.is_owner) { + this.router.transitionTo('authenticated.storeknox.inventory.app-list'); + } + } + model(params: Partial) { const { app_limit, app_offset } = params; - return { - queryParams: { app_limit, app_offset }, - }; + this.skPendingReview + .setLimitOffset({ + limit: app_limit, + offset: app_offset, + }) + .reload(); + + // To get inventory list count in tabs when page is visited for the first time + this.skInventoryAppsService.reload(); } } diff --git a/app/services/sk-inventory-apps.ts b/app/services/sk-inventory-apps.ts new file mode 100644 index 000000000..21bad2060 --- /dev/null +++ b/app/services/sk-inventory-apps.ts @@ -0,0 +1,67 @@ +// eslint-disable-next-line ember/use-ember-data-rfc-395-imports +import { DS } from 'ember-data'; + +import Service, { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import { task } from 'ember-concurrency'; +import type Store from '@ember-data/store'; + +import ENV from 'irene/config/environment'; +import ENUMS from 'irene/enums'; +import type SkInventoryAppModel from 'irene/models/sk-app'; + +type SkInventoryAppModelArray = + DS.AdapterPopulatedRecordArray & { + meta: { count: number }; + }; + +export default class SkInventoryAppService extends Service { + @service declare store: Store; + @service('notifications') declare notify: NotificationService; + + @tracked + skInventoryApps?: DS.AdapterPopulatedRecordArray; + + @tracked limit = 10; + @tracked offset = 0; + @tracked skInventoryAppsCount = 0; + + get isFetchingSkInventoryApps() { + return this.fetch.isRunning; + } + + setLimitOffset({ limit = 10, offset = 0 }) { + this.limit = limit; + this.offset = offset; + + return this; + } + + async reload() { + await this.fetch.perform(); + } + + fetch = task({ keepLatest: true }, async () => { + const queryParams = { + limit: this.limit, + offset: this.offset, + approval_status: ENUMS.SK_APPROVAL_STATUS.APPROVED, + app_status: ENUMS.SK_APP_STATUS.ACTIVE, + }; + + try { + const skInventoryApps = (await this.store.query( + 'sk-app', + queryParams + )) as SkInventoryAppModelArray; + + this.skInventoryApps = skInventoryApps; + this.skInventoryAppsCount = skInventoryApps.meta.count; + } catch { + this.notify.error( + 'Failed to load Inventory Apps Data. Check your network and try again.', + ENV.notifications + ); + } + }); +} diff --git a/app/services/sk-pending-review.ts b/app/services/sk-pending-review.ts index 91d658bfa..9413d90be 100644 --- a/app/services/sk-pending-review.ts +++ b/app/services/sk-pending-review.ts @@ -1,20 +1,20 @@ +// eslint-disable-next-line ember/use-ember-data-rfc-395-imports +import type { DS } from 'ember-data'; import Service, { service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; import { task } from 'ember-concurrency'; -// eslint-disable-next-line ember/use-ember-data-rfc-395-imports -import type { DS } from 'ember-data'; import type Store from '@ember-data/store'; import type RouterService from '@ember/routing/router-service'; import parseError from 'irene/utils/parse-error'; -import type SkAppModel from 'irene/models/sk-app'; import ENUMS from 'irene/enums'; +import type SkAppModel from 'irene/models/sk-app'; type SkPendingReviewResponse = DS.AdapterPopulatedRecordArray & { meta: { count: number }; }; -export default class ServiceAccountService extends Service { +export default class SkPendingReviewService extends Service { @tracked skPendingReviewData: SkPendingReviewResponse | null = null; @tracked singleUpdate: boolean = false; @@ -22,42 +22,40 @@ export default class ServiceAccountService extends Service { @service declare router: RouterService; @service('notifications') declare notify: NotificationService; - fetchPendingReviewApps = task( - { drop: true }, - async ( - limit: number = 10, - offset: number = 0, - setQueryParams: boolean = true - ) => { - try { - if (setQueryParams) { - this.setRouteQueryParams(limit, offset); - } - - this.skPendingReviewData = (await this.store.query('skApp', { - limit, - offset, - approval_status: ENUMS.SK_APPROVAL_STATUS.PENDING_APPROVAL, - app_status: ENUMS.SK_APP_STATUS.ACTIVE, - })) as SkPendingReviewResponse; - - this.singleUpdate = false; - } catch (e) { - this.notify.error(parseError(e)); - } - } - ); + @tracked limit = 10; + @tracked offset = 0; + @tracked skPendingReviewAppsCount = 0; + + get isFetchingSkPendingReviewApps() { + return this.fetchPendingReviewApps.isRunning; + } - get totalCount() { - return this.skPendingReviewData?.meta?.count || 0; + setLimitOffset({ limit = 10, offset = 0 }) { + this.limit = limit; + this.offset = offset; + + return this; } - setRouteQueryParams(limit: number, offset: number) { - this.router.transitionTo({ - queryParams: { - app_limit: limit, - app_offset: offset, - }, - }); + async reload() { + await this.fetchPendingReviewApps.perform(); } + + fetchPendingReviewApps = task({ keepLatest: true }, async () => { + try { + const skPendingReviewData = (await this.store.query('sk-app', { + limit: this.limit, + offset: this.offset, + approval_status: ENUMS.SK_APPROVAL_STATUS.PENDING_APPROVAL, + app_status: ENUMS.SK_APP_STATUS.ACTIVE, + })) as SkPendingReviewResponse; + + this.skPendingReviewData = skPendingReviewData; + this.skPendingReviewAppsCount = skPendingReviewData.meta.count; + + this.singleUpdate = false; + } catch (e) { + this.notify.error(parseError(e)); + } + }); } diff --git a/app/templates/authenticated/storeknox/inventory/app-list.hbs b/app/templates/authenticated/storeknox/inventory/app-list.hbs index 682649dd2..545f2c70e 100644 --- a/app/templates/authenticated/storeknox/inventory/app-list.hbs +++ b/app/templates/authenticated/storeknox/inventory/app-list.hbs @@ -1,3 +1,3 @@ {{page-title 'Requested'}} - \ No newline at end of file + \ No newline at end of file diff --git a/app/templates/authenticated/storeknox/inventory/pending-reviews.hbs b/app/templates/authenticated/storeknox/inventory/pending-reviews.hbs index cf9a15f80..6317dee3e 100644 --- a/app/templates/authenticated/storeknox/inventory/pending-reviews.hbs +++ b/app/templates/authenticated/storeknox/inventory/pending-reviews.hbs @@ -1,3 +1,3 @@ {{page-title (t 'storeknox.pendingReview')}} - \ No newline at end of file + \ No newline at end of file diff --git a/mirage/factories/sk-app.ts b/mirage/factories/sk-app.ts index b82e8f43f..af51de5ba 100644 --- a/mirage/factories/sk-app.ts +++ b/mirage/factories/sk-app.ts @@ -6,8 +6,24 @@ import { faker } from '@faker-js/faker'; import ENUMS from 'irene/enums'; export default Factory.extend({ + approved_on: () => faker.date.past(), + added_on: () => faker.date.past(), + updated_on: () => faker.date.recent(), + rejected_on: () => faker.date.past(), + monitoring_enabled: () => faker.datatype.boolean(), + monitoring_status: () => faker.number.int({ min: 0, max: 3 }), + + app_status: () => faker.helpers.arrayElement(ENUMS.SK_APP_STATUS.BASE_VALUES), + + app_status_display() { + const app_status = this.app_status as number; + + return ENUMS.SK_APP_STATUS.BASE_CHOICES.find((c) => c.value === app_status) + ?.key; + }, + approval_status: () => - faker.helpers.arrayElement(ENUMS.SK_APPROVAL_STATUS.VALUES), + faker.helpers.arrayElement(ENUMS.SK_APPROVAL_STATUS.BASE_VALUES), approval_status_display() { const approval_status = this.approval_status as number; @@ -17,28 +33,17 @@ export default Factory.extend({ )?.key; }, - app_status: () => faker.helpers.arrayElement(ENUMS.SK_APP_STATUS.VALUES), + store_monitoring_status: () => + faker.helpers.arrayElement(ENUMS.SK_APP_MONITORING_STATUS.BASE_VALUES), - app_status_display() { - const app_status = this.app_status as number; + store_monitoring_status_display() { + const store_monitoring_status = this.store_monitoring_status as number; - return ENUMS.SK_APP_STATUS.BASE_CHOICES.find((c) => c.value === app_status) - ?.key; + return ENUMS.SK_APP_MONITORING_STATUS.BASE_CHOICES.find( + (c) => c.value === store_monitoring_status + )?.key; }, - monitoring_enabled: () => faker.datatype.boolean(), - monitoring_status: () => faker.number.int({ min: 0, max: 3 }), - - approved_on: () => faker.date.past(), - added_on: () => faker.date.past(), - updated_on: () => faker.date.recent(), - rejected_on: () => faker.date.past(), - - availability: () => ({ - storeknox: faker.datatype.boolean(), - appknox: faker.datatype.boolean(), - }), - // @ts-expect-error afterCreate(skApp: ModelInstance, server: Server) { // @ts-expect-error @@ -58,4 +63,9 @@ export default Factory.extend({ approval_status: ENUMS.SK_APPROVAL_STATUS.APPROVED, app_status: ENUMS.SK_APP_STATUS.ACTIVE, }), + + withAddedToAppknox: trait({ + approval_status: ENUMS.SK_APPROVAL_STATUS.APPROVED, + app_status: ENUMS.SK_APP_STATUS.ACTIVE, + }), }); diff --git a/mirage/factories/sk-inventory-app.ts b/mirage/factories/sk-inventory-app.ts new file mode 100644 index 000000000..5ad2d147a --- /dev/null +++ b/mirage/factories/sk-inventory-app.ts @@ -0,0 +1,3 @@ +import SkAppFactory from './sk-app'; + +export default SkAppFactory.extend({}); diff --git a/mirage/factories/sk-organization-membership.ts b/mirage/factories/sk-organization-membership.ts new file mode 100644 index 000000000..56ad5e81d --- /dev/null +++ b/mirage/factories/sk-organization-membership.ts @@ -0,0 +1,8 @@ +import { faker } from '@faker-js/faker'; +import ENUMS from 'irene/enums'; +import Base from './base'; + +export default Base.extend({ + roles: () => + faker.helpers.arrayElement(ENUMS.SK_ORGANIZATION_MEMBERSHIP_ROLES.VALUES), +}); diff --git a/mirage/factories/sk-organization.ts b/mirage/factories/sk-organization.ts new file mode 100644 index 000000000..68a1c55cc --- /dev/null +++ b/mirage/factories/sk-organization.ts @@ -0,0 +1,7 @@ +import { faker } from '@faker-js/faker'; +import Base from './base'; + +export default Base.extend({ + add_appknox_project_to_inventory_by_default: faker.datatype.boolean(), + autodiscovery_onboarding_done: faker.datatype.boolean(), +}); diff --git a/mirage/factories/sk-requested-app.ts b/mirage/factories/sk-requested-app.ts index 04d1eb1aa..844686ebb 100644 --- a/mirage/factories/sk-requested-app.ts +++ b/mirage/factories/sk-requested-app.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ // @ts-expect-error "trait" prop missing from miragejs -import { ModelInstance, Server, trait } from 'miragejs'; +import { trait } from 'miragejs'; import { faker } from '@faker-js/faker'; import ENUMS from 'irene/enums'; @@ -28,14 +28,4 @@ export default SkAppFactory.extend({ rejected_by: () => faker.person.firstName(), approval_status: ENUMS.SK_APPROVAL_STATUS.REJECTED, }), - - // @ts-expect-error - afterCreate(skApp: ModelInstance, server: Server) { - // @ts-expect-error - if (!skApp.app_metadata) { - skApp.update({ - app_metadata: server.create('sk-app-metadata'), - }); - } - }, }); diff --git a/mirage/models/organization-user.ts b/mirage/models/organization-user.ts new file mode 100644 index 000000000..db502f142 --- /dev/null +++ b/mirage/models/organization-user.ts @@ -0,0 +1,3 @@ +import { Model } from 'miragejs'; + +export default Model.extend({}); diff --git a/mirage/models/sk-app.ts b/mirage/models/sk-app.ts index c67cfc619..382090817 100644 --- a/mirage/models/sk-app.ts +++ b/mirage/models/sk-app.ts @@ -2,4 +2,5 @@ import { Model, belongsTo } from 'miragejs'; export default Model.extend({ app_metadata: belongsTo('sk-app-metadata'), + addedBy: belongsTo('organization-user'), }); diff --git a/mirage/models/sk-inventory-app.ts b/mirage/models/sk-inventory-app.ts index db502f142..46a269043 100644 --- a/mirage/models/sk-inventory-app.ts +++ b/mirage/models/sk-inventory-app.ts @@ -1,3 +1,7 @@ -import { Model } from 'miragejs'; +import { Model, belongsTo } from 'miragejs'; -export default Model.extend({}); +export default Model.extend({ + app_metadata: belongsTo('sk-app-metadata'), + coreProject: belongsTo('project'), + coreProjectLatestVersion: belongsTo('file'), +}); diff --git a/mirage/models/sk-organization-membership.ts b/mirage/models/sk-organization-membership.ts new file mode 100644 index 000000000..800f6fce9 --- /dev/null +++ b/mirage/models/sk-organization-membership.ts @@ -0,0 +1,6 @@ +import { Model, belongsTo } from 'miragejs'; + +export default Model.extend({ + member: belongsTo('user'), + skOrganization: belongsTo('sk-organization'), +}); diff --git a/mirage/models/sk-organization.ts b/mirage/models/sk-organization.ts new file mode 100644 index 000000000..71d68db08 --- /dev/null +++ b/mirage/models/sk-organization.ts @@ -0,0 +1,6 @@ +import { Model, belongsTo, hasMany } from 'miragejs'; + +export default Model.extend({ + organization: belongsTo('organization'), + members: hasMany('sk-organization-membership'), +}); diff --git a/tests/acceptance/storeknox/inventory-details/app-details-test.js b/tests/acceptance/storeknox/inventory-details/app-details-test.js new file mode 100644 index 000000000..fb9caf397 --- /dev/null +++ b/tests/acceptance/storeknox/inventory-details/app-details-test.js @@ -0,0 +1,386 @@ +import { visit, find, click } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupApplicationTest } from 'ember-qunit'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { t } from 'ember-intl/test-support'; + +import Service from '@ember/service'; +import dayjs from 'dayjs'; + +import { setupRequiredEndpoints } from 'irene/tests/helpers/acceptance-utils'; +import ENUMS from 'irene/enums'; + +// Notification Service +class NotificationsStub extends Service { + errorMsg = null; + successMsg = null; + + error(msg) { + this.errorMsg = msg; + } + + success(msg) { + this.successMsg = msg; + } + + setDefaultAutoClear() {} +} + +module( + 'Acceptance | storeknox/inventory-details/app-details', + function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(async function () { + const { organization, currentOrganizationMe } = + await setupRequiredEndpoints(this.server); + + organization.update({ + features: { + storeknox: true, + }, + }); + + // Server mocks + this.server.get('v2/sk_app_detail/:id', (schema, req) => { + const app = schema.skInventoryApps.find(req.params.id); + + return { ...app.toJSON(), app_metadata: app.app_metadata }; + }); + + this.server.get('/v2/sk_organization', (schema) => { + const skOrganizations = schema.skOrganizations.all().models; + + return { + count: skOrganizations.length, + next: null, + previous: null, + results: skOrganizations, + }; + }); + + this.server.get('/v2/files/:id', (schema, req) => { + return schema.files.find(`${req.params.id}`)?.toJSON(); + }); + + this.server.get('/v2/projects/:id', (schema, req) => { + return schema.projects.find(`${req.params.id}`)?.toJSON(); + }); + + // Services + this.owner.register('service:notifications', NotificationsStub); + const store = this.owner.lookup('service:store'); + + const normalizeSKInventoryApp = (inventoryApp) => + store.push( + store.normalize('sk-inventory-app', { + ...inventoryApp.toJSON(), + app_metadata: inventoryApp.app_metadata, + }) + ); + + this.setProperties({ + currentOrganizationMe, + store, + normalizeSKInventoryApp, + }); + }); + + test.each( + 'it renders app details', + [{ available_on_appknox: true }, { available_on_appknox: false }], + async function (assert, { available_on_appknox }) { + const file = this.server.create('file'); + const core_project = this.server.create('project'); + + // Models + const inventoryApp = this.server.create( + 'sk-inventory-app', + 'withApprovedStatus', + { + core_project: available_on_appknox ? core_project.id : null, + core_project_latest_version: available_on_appknox ? file.id : null, + } + ); + + const inventoryAppRecord = this.normalizeSKInventoryApp(inventoryApp); + + await visit( + `/dashboard/storeknox/inventory-details/${inventoryAppRecord.id}` + ); + + assert + .dom('[data-test-storeknoxInventoryDetails-breadcrumbContainer]') + .exists(); + + if (!available_on_appknox) { + assert + .dom( + '[data-test-storeknoxInventoryDetails-appNotPartOfAppknoxIcon]' + ) + .exists(); + + assert + .dom( + '[data-test-storeknoxInventoryDetails-appNotPartOfAppknoxIconText]' + ) + .hasText(t('storeknox.appNotPartOfAppknox')); + } + + assert + .dom('[data-test-storeknoxInventoryDetails-appLogoImage]') + .hasAttribute('src', inventoryAppRecord.appMetadata.iconUrl); + + assert + .dom('[data-test-storeknoxInventoryDetails-headerAppTitle]') + .hasText(inventoryAppRecord.appMetadata.title); + + assert + .dom('[data-test-storeknoxInventoryDetails-headerAppPackageName]') + .hasText(inventoryAppRecord.appMetadata.packageName); + + // App Logo tests + assert + .dom('[data-test-storeknoxInventoryDetails-headerPlatformIcon]') + .hasClass(inventoryAppRecord.isAndroid ? /android/ : /apple/); + + const storeLogoSelector = inventoryAppRecord.isAndroid + ? '[data-test-storeknoxInventoryDetails-playStoreLogo]' + : '[data-test-storeknoxInventoryDetails-appStoreLogo]'; + + assert.dom(storeLogoSelector).exists(); + + await click(storeLogoSelector); + + assert + .dom('[data-test-storeknox-productInfoCaptionText]') + .hasText(t('infoCapitalCase')); + + const productTitle = inventoryAppRecord.isAndroid + ? t('storeknox.playStore') + : t('storeknox.appStore'); + + assert + .dom('[data-test-storeknox-productInfo-appIsPartOfText]') + .containsText(t('storeknox.appIsPartOf')) + .containsText(productTitle); + + assert + .dom('[data-test-storeknox-productInfo-appStoreLink]') + .hasAttribute('href', inventoryAppRecord.appMetadata.url); + + assert + .dom('[data-test-storeknox-productInfo-appStoreLinkBtn]') + .hasText(t('storeknox.checkOn') + ` ${productTitle}`); + + await click(storeLogoSelector); + + if (available_on_appknox) { + assert.dom('[data-test-storeknoxInventoryDetails-vaptLogo]').exists(); + } + + // Page Info tag + assert + .dom('[data-test-storeknoxInventoryDetails-pageInfoTag]') + .doesNotExist(); + + assert + .dom('[data-test-storeknoxInventoryDetails-monitoringStatusToggle]') + .exists(); + + assert + .dom('[data-test-storeknoxInventoryDetails-monitoringStatusInfoText]') + .hasText(t('storeknox.monitoringAction')); + + // App info + assert + .dom('[data-test-storeknoxInventoryDetails-appDetailsHeaderText]') + .hasText(t('storeknox.appDetails')); + + const appDetailsInfo = [ + { + id: 'developer', + title: t('storeknox.developer'), + value: inventoryAppRecord.devName, + }, + { + id: 'email-id', + title: t('emailId'), + value: inventoryAppRecord.devEmail, + }, + { + id: 'date-added', + title: t('storeknox.addedToInventoryOn'), + value: dayjs(inventoryAppRecord.addedOn).format('DD, MMM YYYY'), + }, + ]; + + appDetailsInfo.forEach((info) => { + const infoElement = find( + `[data-test-storeknoxInventoryDetails-appDetailsHeader='${info.id}']` + ); + + assert + .dom( + '[data-test-storeknoxInventoryDetails-appDetailsHeaderInfoText]', + infoElement + ) + .hasText(info.title); + + assert + .dom( + '[data-test-storeknoxInventoryDetails-appDetailsHeaderInfoValue]', + infoElement + ) + .hasText(info.value || '-'); + }); + } + ); + + // Check for monitoring results pending info + const assertMonitoringPendingInfo = (assert) => { + assert + .dom( + '[data-test-storeknoxInventoryDetails-monitoringResultsPendingWithMonitoringOffIllustration]' + ) + .exists(); + + assert + .dom( + '[data-test-storeknoxInventoryDetails-monitoringResultsPendingOffHeaderText]' + ) + .hasText(t('storeknox.enableMonitoringForApp')); + + assert + .dom( + '[data-test-storeknoxInventoryDetails-monitoringResultsPendingOff]' + ) + .hasText(t('storeknox.monitoringResultsPendingWithMonitoringOff')); + }; + + // Check for monitoring results pending info when monitoring status is off + const assertMonitoringPendingInfoWithMonitoringOff = (assert) => { + assert + .dom( + '[data-test-storeknoxInventoryDetails-monitoringResultsPendingIllustration]' + ) + .exists(); + + assert + .dom( + '[data-test-storeknoxInventoryDetails-monitoringResultsPendingText]' + ) + .hasText(t('storeknox.monitoringResultsPending')); + }; + + test.each( + 'monitoring status toggle', + [ + { turn_on: true }, + { turn_on: false }, + { turn_on: true, cannot_toggle: true }, + ], + async function (assert, { turn_on, cannot_toggle }) { + // Models + if (cannot_toggle) { + this.currentOrganizationMe.update({ + is_owner: false, + is_admin: false, + }); + } + + const inventoryApp = this.server.create( + 'sk-inventory-app', + 'withApprovedStatus', + { + monitoring_enabled: turn_on ? false : true, + + // Meant to test the pending state illustration and text + store_monitoring_status: ENUMS.SK_APP_MONITORING_STATUS.PENDING, + } + ); + + const inventoryAppRecord = this.normalizeSKInventoryApp(inventoryApp); + + // Server mocks + this.server.put( + '/v2/sk_app/:id/update_monitoring_enabled_status', + (schema, req) => { + const { monitoring_enabled } = JSON.parse(req.requestBody); + const app = schema.skInventoryApps.find(req.params.id); + + if (turn_on) { + assert.true(monitoring_enabled); + } else { + assert.false(monitoring_enabled); + } + + return { + ...app.toJSON(), + app_metadata: app.app_metadata, + monitoring_enabled, + }; + } + ); + + await visit( + `/dashboard/storeknox/inventory-details/${inventoryAppRecord.id}` + ); + + assert + .dom('[data-test-storeknoxInventoryDetails-breadcrumbContainer]') + .exists(); + + const statusToggleSelector = + '[data-test-storeknoxInventoryDetails-monitoringStatusToggle] [data-test-toggle-input]'; + + if (cannot_toggle && turn_on) { + assert.dom(statusToggleSelector).isNotChecked().isDisabled(); + + return; + } + + if (turn_on) { + assert.dom(statusToggleSelector).isNotChecked(); + + assertMonitoringPendingInfo(assert); + } else { + assert.dom(statusToggleSelector).isChecked(); + + assertMonitoringPendingInfoWithMonitoringOff(assert); + } + + await click(statusToggleSelector); + + if (turn_on) { + assert.dom(statusToggleSelector).isChecked(); + + assertMonitoringPendingInfoWithMonitoringOff(assert); + } else { + assert.dom(statusToggleSelector).isNotChecked(); + + assertMonitoringPendingInfo(assert); + } + + // TODO: Check chy this isn't working + // Check notification message + // const notify = this.owner.lookup('service:notifications'); + + // assert.strictEqual( + // notify.successMsg, + // t('storeknox.monitoring') + + // ` ${turn_on ? t('enabled') : t('disabled')}` + // ); + } + ); + + test.skip('it renders VA results if core project latest version exists', async function () {}); + + test.skip('it renders upload to appknox CTA if app is not available on appknox', async function () {}); + + test.skip('it renders a "no access state" in latest VA Results when user does not have access to file', async function () {}); + + test.skip('it initiates app upload', async function () {}); + } +); diff --git a/tests/acceptance/storeknox/inventory-details/brand-abuse-test.js b/tests/acceptance/storeknox/inventory-details/brand-abuse-test.js new file mode 100644 index 000000000..675726822 --- /dev/null +++ b/tests/acceptance/storeknox/inventory-details/brand-abuse-test.js @@ -0,0 +1,187 @@ +import { visit, click } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupApplicationTest } from 'ember-qunit'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { t } from 'ember-intl/test-support'; +import Service from '@ember/service'; + +import { setupRequiredEndpoints } from 'irene/tests/helpers/acceptance-utils'; + +// Notification Service +class NotificationsStub extends Service { + errorMsg = null; + successMsg = null; + + error(msg) { + this.errorMsg = msg; + } + + success(msg) { + this.successMsg = msg; + } + + setDefaultAutoClear() {} +} + +module( + 'Acceptance | storeknox/inventory-details/brand-abuse', + function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(async function () { + const { organization, currentOrganizationMe } = + await setupRequiredEndpoints(this.server); + + organization.update({ + features: { + storeknox: true, + }, + }); + + // Server mocks + this.server.get('v2/sk_app_detail/:id', (schema, req) => { + const app = schema.skInventoryApps.find(req.params.id); + + return { ...app.toJSON(), app_metadata: app.app_metadata }; + }); + + this.server.get('/v2/sk_organization', (schema) => { + const skOrganizations = schema.skOrganizations.all().models; + + return { + count: skOrganizations.length, + next: null, + previous: null, + results: skOrganizations, + }; + }); + + this.server.get('/v2/files/:id', (schema, req) => { + return schema.files.find(`${req.params.id}`)?.toJSON(); + }); + + this.server.get('/v2/projects/:id', (schema, req) => { + return schema.projects.find(`${req.params.id}`)?.toJSON(); + }); + + // Services + this.owner.register('service:notifications', NotificationsStub); + const store = this.owner.lookup('service:store'); + + const normalizeSKInventoryApp = (inventoryApp) => + store.push( + store.normalize('sk-inventory-app', { + ...inventoryApp.toJSON(), + app_metadata: inventoryApp.app_metadata, + }) + ); + + this.setProperties({ + currentOrganizationMe, + store, + normalizeSKInventoryApp, + }); + }); + + test.each( + 'it renders app details', + [{ available_on_appknox: true }, { available_on_appknox: false }], + async function (assert, { available_on_appknox }) { + const file = this.server.create('file'); + const core_project = this.server.create('project'); + + // Models + const inventoryApp = this.server.create( + 'sk-inventory-app', + 'withApprovedStatus', + { + core_project: available_on_appknox ? core_project.id : null, + core_project_latest_version: available_on_appknox ? file.id : null, + } + ); + + const inventoryAppRecord = this.normalizeSKInventoryApp(inventoryApp); + + await visit( + `/dashboard/storeknox/inventory-details/${inventoryApp.id}/brand-abuse` + ); + + assert + .dom('[data-test-storeknoxInventoryDetails-breadcrumbContainer]') + .exists(); + + if (!available_on_appknox) { + assert + .dom( + '[data-test-storeknoxInventoryDetails-appNotPartOfAppknoxIcon]' + ) + .exists(); + + assert + .dom( + '[data-test-storeknoxInventoryDetails-appNotPartOfAppknoxIconText]' + ) + .hasText(t('storeknox.appNotPartOfAppknox')); + } + + assert + .dom('[data-test-storeknoxInventoryDetails-appLogoImage]') + .hasAttribute('src', inventoryAppRecord.appMetadata.iconUrl); + + assert + .dom('[data-test-storeknoxInventoryDetails-headerAppTitle]') + .hasText(inventoryAppRecord.appMetadata.title); + + assert + .dom('[data-test-storeknoxInventoryDetails-headerAppPackageName]') + .hasText(inventoryAppRecord.appMetadata.packageName); + + // App Logo tests + assert + .dom('[data-test-storeknoxInventoryDetails-headerPlatformIcon]') + .hasClass(inventoryAppRecord.isAndroid ? /android/ : /apple/); + + const storeLogoSelector = inventoryAppRecord.isAndroid + ? '[data-test-storeknoxInventoryDetails-playStoreLogo]' + : '[data-test-storeknoxInventoryDetails-appStoreLogo]'; + + assert.dom(storeLogoSelector).exists(); + + await click(storeLogoSelector); + + assert + .dom('[data-test-storeknox-productInfoCaptionText]') + .hasText(t('infoCapitalCase')); + + const productTitle = inventoryAppRecord.isAndroid + ? t('storeknox.playStore') + : t('storeknox.appStore'); + + assert + .dom('[data-test-storeknox-productInfo-appIsPartOfText]') + .containsText(t('storeknox.appIsPartOf')) + .containsText(productTitle); + + assert + .dom('[data-test-storeknox-productInfo-appStoreLink]') + .hasAttribute('href', inventoryAppRecord.appMetadata.url); + + assert + .dom('[data-test-storeknox-productInfo-appStoreLinkBtn]') + .hasText(t('storeknox.checkOn') + ` ${productTitle}`); + + await click(storeLogoSelector); + + if (available_on_appknox) { + assert.dom('[data-test-storeknoxInventoryDetails-vaptLogo]').exists(); + } + + // Page Info tag + assert + .dom('[data-test-storeknoxInventoryDetails-pageInfoTag]') + .exists(); + } + ); + } +); diff --git a/tests/acceptance/storeknox/inventory-details/malware-detected-test.js b/tests/acceptance/storeknox/inventory-details/malware-detected-test.js new file mode 100644 index 000000000..1a403c2ee --- /dev/null +++ b/tests/acceptance/storeknox/inventory-details/malware-detected-test.js @@ -0,0 +1,188 @@ +import { visit, click } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupApplicationTest } from 'ember-qunit'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { t } from 'ember-intl/test-support'; + +import Service from '@ember/service'; + +import { setupRequiredEndpoints } from 'irene/tests/helpers/acceptance-utils'; + +// Notification Service +class NotificationsStub extends Service { + errorMsg = null; + successMsg = null; + + error(msg) { + this.errorMsg = msg; + } + + success(msg) { + this.successMsg = msg; + } + + setDefaultAutoClear() {} +} + +module( + 'Acceptance | storeknox/inventory-details/malware-detected', + function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(async function () { + const { organization, currentOrganizationMe } = + await setupRequiredEndpoints(this.server); + + organization.update({ + features: { + storeknox: true, + }, + }); + + // Server mocks + this.server.get('v2/sk_app_detail/:id', (schema, req) => { + const app = schema.skInventoryApps.find(req.params.id); + + return { ...app.toJSON(), app_metadata: app.app_metadata }; + }); + + this.server.get('/v2/sk_organization', (schema) => { + const skOrganizations = schema.skOrganizations.all().models; + + return { + count: skOrganizations.length, + next: null, + previous: null, + results: skOrganizations, + }; + }); + + this.server.get('/v2/files/:id', (schema, req) => { + return schema.files.find(`${req.params.id}`)?.toJSON(); + }); + + this.server.get('/v2/projects/:id', (schema, req) => { + return schema.projects.find(`${req.params.id}`)?.toJSON(); + }); + + // Services + this.owner.register('service:notifications', NotificationsStub); + const store = this.owner.lookup('service:store'); + + const normalizeSKInventoryApp = (inventoryApp) => + store.push( + store.normalize('sk-inventory-app', { + ...inventoryApp.toJSON(), + app_metadata: inventoryApp.app_metadata, + }) + ); + + this.setProperties({ + currentOrganizationMe, + store, + normalizeSKInventoryApp, + }); + }); + + test.each( + 'it renders app details', + [{ available_on_appknox: true }, { available_on_appknox: false }], + async function (assert, { available_on_appknox }) { + const file = this.server.create('file'); + const core_project = this.server.create('project'); + + // Models + const inventoryApp = this.server.create( + 'sk-inventory-app', + 'withApprovedStatus', + { + core_project: available_on_appknox ? core_project.id : null, + core_project_latest_version: available_on_appknox ? file.id : null, + } + ); + + const inventoryAppRecord = this.normalizeSKInventoryApp(inventoryApp); + + await visit( + `/dashboard/storeknox/inventory-details/${inventoryApp.id}/malware-detected` + ); + + assert + .dom('[data-test-storeknoxInventoryDetails-breadcrumbContainer]') + .exists(); + + if (!available_on_appknox) { + assert + .dom( + '[data-test-storeknoxInventoryDetails-appNotPartOfAppknoxIcon]' + ) + .exists(); + + assert + .dom( + '[data-test-storeknoxInventoryDetails-appNotPartOfAppknoxIconText]' + ) + .hasText(t('storeknox.appNotPartOfAppknox')); + } + + assert + .dom('[data-test-storeknoxInventoryDetails-appLogoImage]') + .hasAttribute('src', inventoryAppRecord.appMetadata.iconUrl); + + assert + .dom('[data-test-storeknoxInventoryDetails-headerAppTitle]') + .hasText(inventoryAppRecord.appMetadata.title); + + assert + .dom('[data-test-storeknoxInventoryDetails-headerAppPackageName]') + .hasText(inventoryAppRecord.appMetadata.packageName); + + // App Logo tests + assert + .dom('[data-test-storeknoxInventoryDetails-headerPlatformIcon]') + .hasClass(inventoryAppRecord.isAndroid ? /android/ : /apple/); + + const storeLogoSelector = inventoryAppRecord.isAndroid + ? '[data-test-storeknoxInventoryDetails-playStoreLogo]' + : '[data-test-storeknoxInventoryDetails-appStoreLogo]'; + + assert.dom(storeLogoSelector).exists(); + + await click(storeLogoSelector); + + assert + .dom('[data-test-storeknox-productInfoCaptionText]') + .hasText(t('infoCapitalCase')); + + const productTitle = inventoryAppRecord.isAndroid + ? t('storeknox.playStore') + : t('storeknox.appStore'); + + assert + .dom('[data-test-storeknox-productInfo-appIsPartOfText]') + .containsText(t('storeknox.appIsPartOf')) + .containsText(productTitle); + + assert + .dom('[data-test-storeknox-productInfo-appStoreLink]') + .hasAttribute('href', inventoryAppRecord.appMetadata.url); + + assert + .dom('[data-test-storeknox-productInfo-appStoreLinkBtn]') + .hasText(t('storeknox.checkOn') + ` ${productTitle}`); + + await click(storeLogoSelector); + + if (available_on_appknox) { + assert.dom('[data-test-storeknoxInventoryDetails-vaptLogo]').exists(); + } + + // Page Info tag + assert + .dom('[data-test-storeknoxInventoryDetails-pageInfoTag]') + .exists(); + } + ); + } +); diff --git a/tests/acceptance/storeknox/inventory/app-list-test.js b/tests/acceptance/storeknox/inventory/app-list-test.js new file mode 100644 index 000000000..2bd64839e --- /dev/null +++ b/tests/acceptance/storeknox/inventory/app-list-test.js @@ -0,0 +1,402 @@ +import { visit, findAll, find, triggerEvent, click } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupApplicationTest } from 'ember-qunit'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { t } from 'ember-intl/test-support'; +import { Response } from 'miragejs'; +import Service from '@ember/service'; + +import { setupRequiredEndpoints } from 'irene/tests/helpers/acceptance-utils'; +import { compareInnerHTMLWithIntlTranslation } from 'irene/tests/test-utils'; +import ENUMS from 'irene/enums'; + +// Notification Service +class NotificationsStub extends Service { + errorMsg = null; + successMsg = null; + + error(msg) { + this.errorMsg = msg; + } + + success(msg) { + this.successMsg = msg; + } + + setDefaultAutoClear() {} +} + +module('Acceptance | storeknox/inventory/app-list', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(async function () { + const { organization, currentOrganizationMe } = + await setupRequiredEndpoints(this.server); + + organization.update({ + features: { + storeknox: true, + }, + }); + + // Server mocks + this.server.get('v2/sk_app', () => { + return { + count: 0, + next: null, + previous: null, + results: [], + }; + }); + + this.server.get('/v2/sk_organization', (schema) => { + const skOrganizations = schema.skOrganizations.all().models; + + return { + count: skOrganizations.length, + next: null, + previous: null, + results: skOrganizations, + }; + }); + + // Services + this.owner.register('service:notifications', NotificationsStub); + + this.setProperties({ currentOrganizationMe }); + }); + + test.each( + 'it renders without approved apps', + [{ is_owner: true }, { is_owner: false }], + async function (assert, { is_owner }) { + this.currentOrganizationMe.update({ is_owner }); + + await visit('/dashboard/storeknox/inventory/app-list'); + + assert + .dom('[data-test-storeknoxInventory-inventoryHeaderText]') + .hasText(t('storeknox.inventoryHeader')); + + assert + .dom('[data-test-storeknoxInventory-inventoryDescriptionText]') + .hasText(t('storeknox.inventoryDescription')); + + assert + .dom('[data-test-storeknoxInventory-discoveryPageLink]') + .hasText(t('storeknox.discoverHeader')); + + // Tab items check + const tabItems = [ + { + id: 'app-inventory', + label: t('storeknox.appInventory'), + }, + + // Pending Review apps tab should be displayed for only owners + this.currentOrganizationMe.is_owner && { + id: 'pending-review', + label: t('storeknox.pendingReview'), + }, + ]; + + tabItems + .filter(Boolean) + .forEach((tab) => + assert + .dom(`[data-test-storeknoxInventory-tabs='${tab.id}-tab']`) + .hasText(tab.label) + ); + + // Empty state + assert + .dom( + '[data-test-storeknoxInventory-appListTable-tableEmptyIllustration]' + ) + .exists(); + + assert + .dom('[data-test-storeknoxInventory-appListTable-tableEmptyHeaderText]') + .hasText(t('storeknox.noRequestedAppsFound')); + + compareInnerHTMLWithIntlTranslation(assert, { + message: t('storeknox.noRequestedAppsFoundDescription'), + selector: + '[data-test-storeknoxInventory-appListTable-tableEmptyHeaderDescription]', + }); + + // Settings toggle check + if (is_owner) { + assert + .dom('[data-test-storeknoxInventory-settingsDrawerTrigger]') + .exists(); + } else { + assert + .dom('[data-test-storeknoxInventory-settingsDrawerTrigger]') + .doesNotExist(); + } + } + ); + + test.each( + 'it toggles adding appknox app to inventory by default if user is owner', + [ + { fail: true }, + { + fail: false, + error: { detail: 'disconnect error' }, + }, + ], + async function (assert, { fail, error }) { + assert.expect(); + + this.owner.register('service:notifications', NotificationsStub); + + // Models + this.currentOrganizationMe.update({ is_owner: true }); + + const selectedSkOrg = this.server.create('sk-organization', { + id: 4, + add_appknox_project_to_inventory_by_default: false, + }); + + // Server Mocks + this.server.get('/v2/sk_organization', (schema) => { + const skOrganizations = schema.skOrganizations.all().models; + + return { + count: skOrganizations.length, + next: null, + previous: null, + results: skOrganizations, + }; + }); + + this.server.patch('/v2/sk_organization/:id', (schema, req) => { + if (fail) { + return new Response(400, {}, error); + } + + const { add_appknox_project_to_inventory_by_default } = JSON.parse( + req.requestBody + ); + + // Request is made by the selected org + assert.strictEqual(selectedSkOrg.id, req.params.id); + + // Toggle value is opposite of what is default + assert.strictEqual( + add_appknox_project_to_inventory_by_default, + !selectedSkOrg.add_appknox_project_to_inventory_by_default + ); + + const skOrg = schema.db.skOrganizations.update(`${req.params.id}`, { + add_appknox_project_to_inventory_by_default, + }); + + return skOrg; + }); + + // Test Start + await visit('/dashboard/storeknox/inventory/app-list'); + + assert + .dom('[data-test-storeknoxInventory-settingsDrawerTrigger]') + .exists(); + + await click('[data-test-storeknoxInventory-settingsDrawerTrigger]'); + + assert + .dom('[data-test-storeknoxInventory-settingsDrawer-closeIconBtn]') + .exists(); + + assert + .dom('[data-test-storeknoxInventory-settingsDrawer-title]') + .hasText(t('storeknox.inventorySettings')); + + assert + .dom( + '[data-test-storeknoxInventory-settingsDrawer-addAppknoxProjectsByDefaultTitle]' + ) + .hasText(t('storeknox.addAppknoxProjectsByDefault')); + + assert + .dom( + '[data-test-storeknoxInventory-settingsDrawer-addAppknoxProjectsByDefaultDesc]' + ) + .hasText(t('storeknox.addAppknoxProjectsByDefaultToggleDescription')); + + const addAppknoxProjectsByDefaultToggle = + '[data-test-storeknoxInventory-settingsDrawer-addAppknoxProjectsByDefaultToggle] [data-test-toggle-input]'; + + assert.dom(addAppknoxProjectsByDefaultToggle).isNotChecked(); + + await click(addAppknoxProjectsByDefaultToggle); + + assert.dom(addAppknoxProjectsByDefaultToggle).isChecked(); + + // const notify = this.owner.lookup('service:notifications'); + + // if (fail) { + // assert.strictEqual( + // notify.successMsg, + // t('storeknox.addAppknoxProjectsByDefaultSuccessMessage') + // ); + // } else { + // assert.strictEqual(notify.errorMsg, error.detail); + // } + } + ); + + test.each( + 'it renders with approved apps (With Action Required and No Action Required)', + [ + { + with_action_required: true, + store_monitoring_status: ENUMS.SK_APP_MONITORING_STATUS.UNSCANNED, + }, + { + with_action_required: false, + store_monitoring_status: ENUMS.SK_APP_MONITORING_STATUS.PENDING, + }, + ], + async function (assert, { with_action_required, store_monitoring_status }) { + const inventoryApps = this.server.createList( + 'sk-inventory-app', + 3, + 'withApprovedStatus', + { store_monitoring_status } + ); + + // Server mocks + this.server.get('v2/sk_app', (schema, req) => { + const { app_status, approval_status } = req.queryParams; + + const inventoryApps = schema.skInventoryApps + .where( + (a) => + a.app_status === Number(app_status) && + a.approval_status === Number(approval_status) + ) + .models.map((a) => ({ + ...a.toJSON(), + app_metadata: a.app_metadata, + })); + + return { + count: inventoryApps.length, + next: null, + previous: null, + results: inventoryApps, + }; + }); + + await visit('/dashboard/storeknox/inventory/app-list'); + + const appElementList = findAll( + '[data-test-storeknoxInventory-appListTable-row]' + ); + + // Contains the right number of apps + assert.strictEqual(appElementList.length, inventoryApps.length); + + // Sanity check for rendered apps + for (let index = 0; index < inventoryApps.length; index++) { + const iApp = inventoryApps[index]; + + const iAppElement = find( + `[data-test-storeknoxInventory-appListTable-rowId='${iApp.id}']` + ); + + const metadata = iApp.app_metadata; + + if (iApp.platform === ENUMS.PLATFORM.ANDROID) { + assert + .dom( + '[data-test-storeknoxTableColumns-store-playStoreIcon]', + iAppElement + ) + .exists(); + } + + if (iApp.platform === ENUMS.PLATFORM.IOS) { + assert + .dom('[data-test-storeknoxTableColumns-store-iosIcon]', iAppElement) + .exists(); + } + + assert + .dom( + '[data-test-storeknoxTableColumns-applicationDevName]', + iAppElement + ) + .hasText(metadata.dev_name); + + assert + .dom( + '[data-test-storeknoxTableColumns-applicationDevEmail]', + iAppElement + ) + .hasText(metadata.dev_email); + + assert + .dom( + '[data-test-storeknoxTableColumns-applicationTitle]', + iAppElement + ) + .hasText(metadata.title); + + assert + .dom( + '[data-test-storeknoxTableColumns-applicationPackageName]', + iAppElement + ) + .hasText(metadata.package_name); + + assert + .dom('[data-test-applogo-img]', iAppElement) + .hasAttribute('src', metadata.icon_url); + + assert + .dom( + '[data-test-storeknoxInventory-appListTable-monitoringStatusIcon]', + iAppElement + ) + .hasClass(with_action_required ? /error/ : /success/) + .hasClass(with_action_required ? /info/ : /check-circle/); + + // Check for needs action tooltip + const tooltipTrigger = + '[data-test-storeknoxInventory-appListTable-monitoringStatusIcon]'; + + const needsActionTooltipTriggerElement = find(tooltipTrigger); + + const tooltipContentSelector = '[data-test-ak-tooltip-content]'; + + await triggerEvent(needsActionTooltipTriggerElement, 'mouseenter'); + + assert.dom(tooltipContentSelector).exists(); + + assert + .dom( + '[data-test-storeknoxInventory-appListTable-monitoringStatusTooltipHeaderText]' + ) + .hasText(t('reason')); + + assert + .dom( + '[data-test-storeknoxInventory-appListTable-monitoringStatusTooltipText]' + ) + .containsText( + with_action_required + ? t('storeknox.actionsNeededMsg') + : t('storeknox.noActionsNeededMsg') + ) + .containsText(t('storeknox.haveBeenDetected')); + + await triggerEvent(needsActionTooltipTriggerElement, 'mouseleave'); + } + } + ); +}); diff --git a/tests/acceptance/storeknox/inventory/pending-review-test.js b/tests/acceptance/storeknox/inventory/pending-review-test.js new file mode 100644 index 000000000..bb2385a28 --- /dev/null +++ b/tests/acceptance/storeknox/inventory/pending-review-test.js @@ -0,0 +1,449 @@ +import { visit, findAll, find, triggerEvent, click } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupApplicationTest } from 'ember-qunit'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { t } from 'ember-intl/test-support'; +import Service from '@ember/service'; +import dayjs from 'dayjs'; + +import { setupRequiredEndpoints } from 'irene/tests/helpers/acceptance-utils'; +import ENUMS from 'irene/enums'; + +// Notification Service +class NotificationsStub extends Service { + errorMsg = null; + successMsg = null; + + error(msg) { + this.errorMsg = msg; + } + + success(msg) { + this.successMsg = msg; + } + + setDefaultAutoClear() {} +} + +module('Acceptance | storeknox/inventory/pending-review', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(async function () { + const { organization, currentOrganizationMe } = + await setupRequiredEndpoints(this.server); + + // Models + organization.update({ + features: { + storeknox: true, + }, + }); + + currentOrganizationMe.update({ is_owner: true }); + + this.server.create('sk-organization'); + + // Services + this.owner.register('service:notifications', NotificationsStub); + + this.setProperties({ currentOrganizationMe }); + }); + + test('it renders with empty state', async function (assert) { + this.server.create('sk-organization'); + + // Server mocks + this.server.get('/v2/sk_app', () => { + return { + count: 0, + next: null, + previous: null, + results: [], + }; + }); + + this.server.get('/v2/sk_organization', (schema) => { + const skOrganizations = schema.skOrganizations.all().models; + + return { + count: skOrganizations.length, + next: null, + previous: null, + results: skOrganizations, + }; + }); + + // Test start + await visit('/dashboard/storeknox/inventory/pending-reviews'); + + assert + .dom('[data-test-storeknoxInventory-pendingReviewEmptyIllustration]') + .exists(); + + assert + .dom('[data-test-storeknoxInventory-pendingReviewEmptyHeaderText]') + .hasText(t('storeknox.noPendingItems')); + + assert + .dom('[data-test-storeknoxInventory-pendingReviewEmptyDescriptionText]') + .hasText(t('storeknox.noPendingItemsDescription')); + }); + + test.each( + 'it renders with pending review apps', + [ + { + manual_discovery: true, + app_source: ENUMS.SK_DISCOVERY.MANUAL, + }, + { + auto_discovery: true, + app_source: ENUMS.SK_DISCOVERY.AUTO, + }, + ], + async function (assert, { manual_discovery, app_source }) { + // Models + const added_by = this.server.create('organization-user'); + + const pendingReviewApp = this.server.create( + 'sk-app', + { + app_source, + added_by: manual_discovery ? added_by.id : null, + }, + 'withPendingReviewStatus' + ); + + // Server mocks + this.server.get('v2/sk_app', (schema, req) => { + const { app_status, approval_status } = req.queryParams; + + const skApps = schema.skApps + .where( + (a) => + a.app_status === Number(app_status) && + a.approval_status === Number(approval_status) + ) + .models.map((a) => ({ + ...a.toJSON(), + app_metadata: a.app_metadata, + })); + + return { + count: skApps.length, + next: null, + previous: null, + results: skApps, + }; + }); + + this.server.get('/v2/sk_organization', (schema) => { + const skOrganizations = schema.skOrganizations.all().models; + + return { + count: skOrganizations.length, + next: null, + previous: null, + results: skOrganizations, + }; + }); + + // Test start + await visit('/dashboard/storeknox/inventory/pending-reviews'); + + const appElementList = findAll( + '[data-test-storeknoxInventory-pendingReviewTable-row]' + ); + + // Contains the right number of apps + assert.strictEqual(appElementList.length, 1); + + const metadata = pendingReviewApp.app_metadata; + + const prAppElement = find( + `[data-test-storeknoxInventory-pendingReviewTable-rowId='${pendingReviewApp.id}']` + ); + + if (pendingReviewApp.platform === ENUMS.PLATFORM.ANDROID) { + assert + .dom( + '[data-test-storeknoxTableColumns-store-playStoreIcon]', + prAppElement + ) + .exists(); + } + + if (pendingReviewApp.platform === ENUMS.PLATFORM.IOS) { + assert + .dom('[data-test-storeknoxTableColumns-store-iosIcon]', prAppElement) + .exists(); + } + + assert + .dom('[data-test-storeknoxTableColumns-applicationTitle]', prAppElement) + .hasText(metadata.title); + + assert + .dom( + '[data-test-storeknoxTableColumns-applicationPackageName]', + prAppElement + ) + .hasText(metadata.package_name); + + assert + .dom('[data-test-applogo-img]', prAppElement) + .hasAttribute('src', metadata.icon_url); + + // Requester Checks + if (manual_discovery) { + assert + .dom( + '[data-test-storeknoxInventory-pendingReviewTable-requesterEmail]' + ) + .hasText(added_by.email); + } else { + assert + .dom( + '[data-test-storeknoxInventory-pendingReviewTable-systemRequested]' + ) + .hasText(t('system')); + } + + // Check for needs action tooltip + const tooltipTrigger = + '[data-test-storeknoxInventory-pendingReviewTable-requestByInfoTrigger]'; + + const requesterInfoTooltipTriggerElement = find(tooltipTrigger); + const tooltipContentSelector = '[data-test-ak-tooltip-content]'; + + await triggerEvent(requesterInfoTooltipTriggerElement, 'mouseenter'); + + assert.dom(tooltipContentSelector).exists(); + + assert + .dom( + '[data-test-storeknoxInventory-pendingReviewTable-requestedOnHeaderText]' + ) + .hasText(t('requestedOn')); + + assert + .dom('[data-test-storeknoxInventory-pendingReviewTable-addedOnDate]') + .hasText( + dayjs(pendingReviewApp.added_on).format('MMMM D, YYYY, HH:mm') + ); + + assert + .dom('[data-test-storeknoxInventory-pendingReviewTable-foundByHeader]') + .hasText(t('type')); + + assert + .dom('[data-test-storeknoxInventory-pendingReviewTable-foundBy]') + .hasText( + manual_discovery + ? t('storeknox.manualDiscovery') + : t('storeknox.autoDiscovery') + ); + + await triggerEvent(requesterInfoTooltipTriggerElement, 'mouseleave'); + + // Approve/Reject Btns + assert + .dom('[data-test-storeknoxInventory-pendingReviewTable-approveBtn]') + .exists(); + + assert + .dom('[data-test-storeknoxInventory-pendingReviewTable-approveBtnIcon]') + .exists(); + + assert + .dom('[data-test-storeknoxInventory-pendingReviewTable-rejectBtn]') + .exists(); + + assert + .dom('[data-test-storeknoxInventory-pendingReviewTable-rejectBtnIcon]') + .exists(); + } + ); + + test.each( + 'it rejects/accepts an app', + [ + { + reject: true, + approval_status: ENUMS.SK_APPROVAL_STATUS.REJECTED, + approval_status_display: 'REJECTED', + }, + { + accept: true, + approval_status: ENUMS.SK_APPROVAL_STATUS.APPROVED, + approval_status_display: 'APPROVED', + }, + ], + async function ( + assert, + { reject, accept, approval_status, approval_status_display } + ) { + // Models + const pendingReviewApps = this.server.createList( + 'sk-app', + 3, + { + added_by: this.server.create('organization-user').id, + }, + 'withPendingReviewStatus' + ); + + const appToRejectOrAccept = pendingReviewApps[0]; + + // Server mocks + this.server.get('v2/sk_app', (schema, req) => { + const { app_status, approval_status } = req.queryParams; + + const skApps = schema.skApps + .where( + (a) => + a.app_status === Number(app_status) && + a.approval_status === Number(approval_status) + ) + .models.map((a) => ({ + ...a.toJSON(), + app_metadata: a.app_metadata, + })); + + return { + count: skApps.length, + next: null, + previous: null, + results: skApps, + }; + }); + + this.server.get('/v2/sk_organization', (schema) => { + const skOrganizations = schema.skOrganizations.all().models; + + return { + count: skOrganizations.length, + next: null, + previous: null, + results: skOrganizations, + }; + }); + + this.server.put( + '/v2/sk_app/:id/update_approval_status', + (schema, req) => { + const reqBody = JSON.parse(req.requestBody); + const skAppId = req.params.id; + + // Check if correct app is being rejected or approved + assert.strictEqual(appToRejectOrAccept.id, skAppId); + assert.strictEqual(reqBody.approval_status, Number(approval_status)); + + // Update DB to reflect app update + schema.db.skApps.update(`${req.params.id}`, { + approval_status, + }); + + return { + id: skAppId, + approval_status: reqBody.approval_status, + approval_status_display, + }; + } + ); + + // Test start + await visit('/dashboard/storeknox/inventory/pending-reviews'); + + let pendingReviewAppElements = findAll( + '[data-test-storeknoxInventory-pendingReviewTable-row]' + ); + + assert.strictEqual( + pendingReviewAppElements.length, + pendingReviewApps.length + ); + + const appToRejectOrAcceptElement = find( + `[data-test-storeknoxInventory-pendingReviewTable-rowId='${appToRejectOrAccept.id}']` + ); + + // Reject an app + if (reject) { + assert + .dom( + '[data-test-storeknoxInventory-pendingReviewTable-rejectBtn]', + appToRejectOrAcceptElement + ) + .exists(); + + assert + .dom( + '[data-test-storeknoxInventory-pendingReviewTable-rejectBtnIcon]', + appToRejectOrAcceptElement + ) + .exists(); + + await click( + appToRejectOrAcceptElement.querySelector( + '[data-test-storeknoxInventory-pendingReviewTable-rejectBtn]' + ) + ); + } + + // Approve an app + if (accept) { + assert + .dom( + '[data-test-storeknoxInventory-pendingReviewTable-approveBtn]', + appToRejectOrAcceptElement + ) + .exists(); + + assert + .dom( + '[data-test-storeknoxInventory-pendingReviewTable-approveBtnIcon]', + appToRejectOrAcceptElement + ) + .exists(); + + await click( + appToRejectOrAcceptElement.querySelector( + '[data-test-storeknoxInventory-pendingReviewTable-approveBtn]' + ) + ); + } + + // Check list again + pendingReviewAppElements = findAll( + '[data-test-storeknoxInventory-pendingReviewTable-row]' + ); + + assert.strictEqual( + pendingReviewAppElements.length, + pendingReviewApps.length - 1 + ); + + assert + .dom( + `[data-test-storeknoxInventory-pendingReviewTable-rowId='${appToRejectOrAccept.id}']` + ) + .doesNotExist(); + + // Check notification message + const notify = this.owner.lookup('service:notifications'); + + const appName = { + appName: appToRejectOrAccept.app_metadata.title, + }; + + assert.strictEqual( + notify.successMsg, + reject + ? t('storeknox.appRejected', appName) + : t('storeknox.appAddedToInventory', appName) + ); + } + ); +}); diff --git a/translations/en.json b/translations/en.json index 88a7b29a3..52f0112ec 100644 --- a/translations/en.json +++ b/translations/en.json @@ -625,6 +625,7 @@ "inactiveCapital": "Inactive", "includeInactiveMembers": "Include inactive users", "info": "Info", + "infoCapitalCase": "INFO", "installAppknoxPluginTo": "Install plugin to ", "inProgress": "In Progress", "integrateAppknoxTo": "Integrate to ", @@ -1585,14 +1586,22 @@ "malwareDetected": "Malware Detected", "unscannedVersion": "Unscanned Version", "brandAbuse": "Brand Abuse", + "monitoring": "Monitoring", "appNotInAppknoxHeader": "This app has never been uploaded to Appknox.", "appNotInAppknoxDescription": "It is strongly recommended to upload this to Appknox to perform the Vulnerability Assessment.", "monitoringResultsPending": "Monitoring activity for this newly added app has been initiated. Please come back after some time to see the results.", + "enableMonitoringForApp": "Enable monitoring for this app", + "monitoringResultsPendingWithMonitoringOff": "Use the toggle on this page to enable monitoring of this app. Once done, come back to this page after some time to view the results.", "storeMonitoringOverview": "Store Monitoring Overview", "inventorySettings": "Inventory Settings", "addAppknoxProjectsByDefault": "Add Appknox Projects to Inventory by default", "addAppknoxProjectsByDefaultSuccessMessage": "Status Updated Successfully.", - "addAppknoxProjectsByDefaultToggleDescription": "Turning this toggle ON would ensure any new app/namespace uploaded in your Appknox account, reflects in your Org Inventory automatically" + "addAppknoxProjectsByDefaultToggleDescription": "Turning this toggle ON would ensure any new app/namespace uploaded in your Appknox account, reflects in your Org Inventory automatically", + "monitoringAction": "Monitoring Action", + "brandAbuseFeatureUnavailableHeader": "Coming Soon - Detection of Brand Abuse", + "brandAbuseFeatureUnavailableSubText": "Identify fake apps that are trying to mimicking your apps across multiple stores.", + "malwareDetectedFeatureUnavailableHeader": "Unlock the Malware feature soon", + "malwareDetectedFeatureUnavailableSubText": "Identify fake apps that are trying to mimicking your apps across multiple stores." }, "storeLowercase": "store", "submit": "Submit", diff --git a/translations/ja.json b/translations/ja.json index 1f81cbf9e..3766a2279 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -625,6 +625,7 @@ "inactiveCapital": "Inactive", "includeInactiveMembers": "Include inactive users", "info": "Info", + "infoCapitalCase": "INFO", "installAppknoxPluginTo": "Install plugin to ", "inProgress": "In Progress", "integrateAppknoxTo": "Integrate to ", @@ -1585,14 +1586,22 @@ "malwareDetected": "Malware Detected", "unscannedVersion": "Unscanned Version", "brandAbuse": "Brand Abuse", + "monitoring": "Monitoring", "appNotInAppknoxHeader": "This app has never been uploaded to Appknox.", "appNotInAppknoxDescription": "It is strongly recommended to upload this to Appknox to perform the Vulnerability Assessment.", "monitoringResultsPending": "Monitoring activity for this newly added app has been initiated. Please come back after some time to see the results.", + "enableMonitoringForApp": "Enable monitoring for this app", + "monitoringResultsPendingWithMonitoringOff": "Use the toggle on this page to enable monitoring of this app. Once done, come back to this page after some time to view the results.", "storeMonitoringOverview": "Store Monitoring Overview", "inventorySettings": "Inventory Settings", "addAppknoxProjectsByDefault": "Add Appknox Projects to Inventory by default", "addAppknoxProjectsByDefaultSuccessMessage": "Status Updated Successfully.", - "addAppknoxProjectsByDefaultToggleDescription": "Turning this toggle ON would ensure any new app/namespace uploaded in your Appknox account, reflects in your Org Inventory automatically" + "addAppknoxProjectsByDefaultToggleDescription": "Turning this toggle ON would ensure any new app/namespace uploaded in your Appknox account, reflects in your Org Inventory automatically", + "monitoringAction": "Monitoring Action", + "brandAbuseFeatureUnavailableHeader": "Coming Soon - Detection of Brand Abuse", + "brandAbuseFeatureUnavailableSubText": "Identify fake apps that are trying to mimicking your apps across multiple stores.", + "malwareDetectedFeatureUnavailableHeader": "Unlock the Malware feature soon", + "malwareDetectedFeatureUnavailableSubText": "Identify fake apps that are trying to mimicking your apps across multiple stores." }, "storeLowercase": "store", "submit": "Submit", diff --git a/types/ak-svg.d.ts b/types/ak-svg.d.ts index 548d2b704..b7feab6b6 100644 --- a/types/ak-svg.d.ts +++ b/types/ak-svg.d.ts @@ -77,7 +77,8 @@ export enum AkSvgComponentInvocationByNames { SoxMalwareFeatureAbsence, SoxUnscannedDetailsTableEmpty, SoxUnscannedHistoryTableEmpty, - SoxMonitoringPending, + SoxMonitoringResultsPending, + SoxMonitoringResultsPendingWithMonitoringOff, } export enum AkSvgComponentInvocationByPaths { diff --git a/types/global.d.ts b/types/global.d.ts index 5fc763513..633b26d83 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -63,9 +63,23 @@ declare module '@glint/environment-ember-loose/registry' { Return: boolean; }>; + gt: HelperLike<{ + Args: { + Positional: [string | number, string | number]; + }; + Return: boolean; + }>; + + or: HelperLike<{ + Args: { + Positional: (string | number | boolean)[]; + }; + Return: boolean; + }>; + and: HelperLike<{ Args: { - Positional: unknown[]; + Positional: (string | number | boolean)[]; }; Return: boolean; }>;