diff --git a/app/adapters/sk-add-to-inventory.ts b/app/adapters/sk-add-to-inventory.ts new file mode 100644 index 000000000..20889a84e --- /dev/null +++ b/app/adapters/sk-add-to-inventory.ts @@ -0,0 +1,15 @@ +import CommonDRFAdapter from './commondrf'; + +export default class SkAddToInventoryAdapter extends CommonDRFAdapter { + _buildURL() { + const baseurl = `${this.namespace_v2}/sk_app`; + + return this.buildURLFromBase(baseurl); + } +} + +declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + 'sk-add-to-inventory': SkAddToInventoryAdapter; + } +} diff --git a/app/adapters/sk-app.ts b/app/adapters/sk-app.ts new file mode 100644 index 000000000..6ec5a142a --- /dev/null +++ b/app/adapters/sk-app.ts @@ -0,0 +1,36 @@ +import SkAppModel from 'irene/models/sk-app'; +import CommonDRFAdapter from './commondrf'; + +export default class SkAppAdapter extends CommonDRFAdapter { + _buildURL() { + const baseurl = `${this.namespace_v2}/sk_app`; + + return this.buildURLFromBase(baseurl); + } + + async approveApp(id: string): Promise { + const url = this.buildURL().concat(`/${id}/update_approval_status`); + + const data = { + approval_status: 1, + }; + + return await this.ajax(url, 'PUT', { data }); + } + + async rejectApp(id: string): Promise { + const url = this.buildURL().concat(`/${id}/update_approval_status`); + + const data = { + approval_status: 2, + }; + + return await this.ajax(url, 'PUT', { data }); + } +} + +declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + 'sk-app': SkAppAdapter; + } +} diff --git a/app/adapters/sk-discovery-result.ts b/app/adapters/sk-discovery-result.ts new file mode 100644 index 000000000..78ba61f04 --- /dev/null +++ b/app/adapters/sk-discovery-result.ts @@ -0,0 +1,15 @@ +import CommonDRFAdapter from './commondrf'; + +export default class SkDiscoveryResultAdapter extends CommonDRFAdapter { + urlForQuery(q: { id: string }) { + const url = `${this.namespace_v2}/sk_discovery/${q.id}/search_results`; + + return this.buildURLFromBase(url); + } +} + +declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + 'sk-discovery-result': SkDiscoveryResultAdapter; + } +} diff --git a/app/adapters/sk-discovery.ts b/app/adapters/sk-discovery.ts new file mode 100644 index 000000000..37d23fa2a --- /dev/null +++ b/app/adapters/sk-discovery.ts @@ -0,0 +1,15 @@ +import CommonDRFAdapter from './commondrf'; + +export default class SkDiscoveryAdapter extends CommonDRFAdapter { + _buildURL() { + const baseurl = `${this.namespace_v2}/sk_discovery`; + + return this.buildURLFromBase(baseurl); + } +} + +declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + 'sk-discovery': SkDiscoveryAdapter; + } +} diff --git a/app/adapters/sk-inventory-approval-status.ts b/app/adapters/sk-inventory-approval-status.ts new file mode 100644 index 000000000..2587b0049 --- /dev/null +++ b/app/adapters/sk-inventory-approval-status.ts @@ -0,0 +1,15 @@ +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/adapters/sk-requested-app.ts b/app/adapters/sk-requested-app.ts new file mode 100644 index 000000000..e8388a81e --- /dev/null +++ b/app/adapters/sk-requested-app.ts @@ -0,0 +1,15 @@ +import CommonDRFAdapter from './commondrf'; + +export default class SkRequestedAppAdapter extends CommonDRFAdapter { + _buildURL() { + const baseurl = `${this.namespace_v2}/sk_requested_apps`; + + return this.buildURLFromBase(baseurl); + } +} + +declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + 'sk-requested-app': SkRequestedAppAdapter; + } +} diff --git a/app/components/ak-svg/info-indicator.hbs b/app/components/ak-svg/info-indicator.hbs new file mode 100644 index 000000000..6a369112f --- /dev/null +++ b/app/components/ak-svg/info-indicator.hbs @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/app/components/ak-svg/no-pending-items.hbs b/app/components/ak-svg/no-pending-items.hbs new file mode 100644 index 000000000..3dd0ec7ce --- /dev/null +++ b/app/components/ak-svg/no-pending-items.hbs @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/components/ak-svg/sm-indicator.hbs b/app/components/ak-svg/sm-indicator.hbs new file mode 100644 index 000000000..ff02360c7 --- /dev/null +++ b/app/components/ak-svg/sm-indicator.hbs @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/app/components/ak-svg/storeknox-playstore-logo.hbs b/app/components/ak-svg/storeknox-playstore-logo.hbs new file mode 100644 index 000000000..a4aeb556f --- /dev/null +++ b/app/components/ak-svg/storeknox-playstore-logo.hbs @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/components/ak-svg/storeknox-search-apps.hbs b/app/components/ak-svg/storeknox-search-apps.hbs new file mode 100644 index 000000000..c45edbffd --- /dev/null +++ b/app/components/ak-svg/storeknox-search-apps.hbs @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/components/ak-svg/vapt-indicator.hbs b/app/components/ak-svg/vapt-indicator.hbs new file mode 100644 index 000000000..51faaf94f --- /dev/null +++ b/app/components/ak-svg/vapt-indicator.hbs @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/app/components/ak-svg/welcome-to-storeknox.hbs b/app/components/ak-svg/welcome-to-storeknox.hbs new file mode 100644 index 000000000..f22c8dddd --- /dev/null +++ b/app/components/ak-svg/welcome-to-storeknox.hbs @@ -0,0 +1,2013 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/components/page-wrapper/index.hbs b/app/components/page-wrapper/index.hbs new file mode 100644 index 000000000..575799360 --- /dev/null +++ b/app/components/page-wrapper/index.hbs @@ -0,0 +1,5 @@ +
+
+ {{yield}} +
+
\ No newline at end of file diff --git a/app/components/page-wrapper/index.scss b/app/components/page-wrapper/index.scss new file mode 100644 index 000000000..5b9cb325a --- /dev/null +++ b/app/components/page-wrapper/index.scss @@ -0,0 +1,12 @@ +.page-wrapper-root { + padding: 0 1.5em 1.5em 1.5em; + margin: -0.5em; + background-color: var(--page-wrapper-background-color); + min-height: calc(100vh - 56px); + + .page-wrapper-container { + max-width: 1200px; + margin: 0 auto; + width: 100%; + } +} diff --git a/app/components/page-wrapper/index.ts b/app/components/page-wrapper/index.ts new file mode 100644 index 000000000..8fea483e3 --- /dev/null +++ b/app/components/page-wrapper/index.ts @@ -0,0 +1,15 @@ +import Component from '@glimmer/component'; + +interface PageWrapperSignature { + Blocks: { + default: []; + }; +} + +export default class PageWrapperComponent extends Component {} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + PageWrapper: typeof PageWrapperComponent; + } +} diff --git a/app/components/storeknox/discover/index.hbs b/app/components/storeknox/discover/index.hbs new file mode 100644 index 000000000..17163d19c --- /dev/null +++ b/app/components/storeknox/discover/index.hbs @@ -0,0 +1,48 @@ +{{!-- + + --}} + + + + {{t 'storeknox.discoverHeader'}} + + + + {{t 'storeknox.discoverDescription'}} + + + + + {{#each this.tabItems as |item|}} + + {{item.label}} + + {{/each}} + + +{{#if this.showWelcomeModal}} + + + +{{/if}} \ No newline at end of file diff --git a/app/components/storeknox/discover/index.scss b/app/components/storeknox/discover/index.scss new file mode 100644 index 000000000..998031e65 --- /dev/null +++ b/app/components/storeknox/discover/index.scss @@ -0,0 +1,14 @@ +.header-storeknox-discover-page { + margin: 0.714em 0; + border: 1px solid var(--storeknox-discover-header-border-color); + background-color: var(--storeknox-discover-header-background-color); + padding: 1.428em; +} + +.description-storeknox-discover-page { + color: var(--storeknox-discover-header-description-color); +} + +.discover-tabs { + margin-bottom: 1.428em; +} diff --git a/app/components/storeknox/discover/index.ts b/app/components/storeknox/discover/index.ts new file mode 100644 index 000000000..efcc4aeae --- /dev/null +++ b/app/components/storeknox/discover/index.ts @@ -0,0 +1,58 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import IntlService from 'ember-intl/services/intl'; +import MeService from 'irene/services/me'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import SkPendingReviewService from 'irene/services/sk-pending-review'; + +export default class StoreknoxDiscoverComponent extends Component { + @service declare intl: IntlService; + @service declare me: MeService; + @service declare skPendingReview: SkPendingReviewService; + + @tracked showWelcomeModal = false; + + constructor(owner: unknown, args: object) { + super(owner, args); + + if (!this.me.org?.is_admin) { + this.showWelcomeModal = false; + } else { + this.skPendingReview.fetchPendingReviewApps.perform(); + } + } + + get tabItems() { + return [ + { + id: 'discovery-results', + route: 'authenticated.storeknox.discover.result', + label: this.intl.t('storeknox.discoveryResults'), + }, + this.me.org?.is_admin + ? { + id: 'pending-review', + route: 'authenticated.storeknox.discover.review', + label: this.intl.t('storeknox.pendingReview'), + hasBadge: true, + badgeCount: this.skPendingReview.totalCount, + } + : { + id: 'requested-apps', + route: 'authenticated.storeknox.discover.requested', + label: this.intl.t('storeknox.requestedApps'), + }, + ].filter(Boolean); + } + + @action closeWelcomeModal() { + this.showWelcomeModal = false; + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Storeknox::Discover': typeof StoreknoxDiscoverComponent; + } +} diff --git a/app/components/storeknox/discover/pending-review/empty/index.hbs b/app/components/storeknox/discover/pending-review/empty/index.hbs new file mode 100644 index 000000000..7c859ce1a --- /dev/null +++ b/app/components/storeknox/discover/pending-review/empty/index.hbs @@ -0,0 +1,12 @@ + + + + + + {{t 'storeknox.noPendingItems'}} + + + + {{t 'storeknox.noPendingItemsDescription'}} + + \ No newline at end of file diff --git a/app/components/storeknox/discover/pending-review/empty/index.scss b/app/components/storeknox/discover/pending-review/empty/index.scss new file mode 100644 index 000000000..da3257899 --- /dev/null +++ b/app/components/storeknox/discover/pending-review/empty/index.scss @@ -0,0 +1,13 @@ +.empty-container { + margin-top: 5em; + margin-bottom: 5em; + + .header { + margin-top: 1.5625em; + } + + .body-text { + text-align: center; + max-width: 25em; + } +} diff --git a/app/components/storeknox/discover/pending-review/empty/index.ts b/app/components/storeknox/discover/pending-review/empty/index.ts new file mode 100644 index 000000000..27f3746e0 --- /dev/null +++ b/app/components/storeknox/discover/pending-review/empty/index.ts @@ -0,0 +1,9 @@ +import Component from '@glimmer/component'; + +export default class StoreknoxDiscoverPendingReviewEmptyComponent extends Component {} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Storeknox::Discover::PendingReview::Empty': typeof StoreknoxDiscoverPendingReviewEmptyComponent; + } +} diff --git a/app/components/storeknox/discover/pending-review/index.hbs b/app/components/storeknox/discover/pending-review/index.hbs new file mode 100644 index 000000000..1ca192936 --- /dev/null +++ b/app/components/storeknox/discover/pending-review/index.hbs @@ -0,0 +1,66 @@ +{{!-- +
+ + <:rightAdornment> + + + +
+ + + {{#if this.showButtons}} + + <:leftIcon> + + + + <:default> + {{t 'approve'}} + + + + + <:leftIcon> + + + + <:default> + {{t 'reject'}} + + + + + {{/if}} + + + + {{t 'storeknox.logs'}} + + + +
--}} + +{{#if this.showTable}} + +{{else}} + +{{/if}} \ No newline at end of file diff --git a/app/components/storeknox/discover/pending-review/index.scss b/app/components/storeknox/discover/pending-review/index.scss new file mode 100644 index 000000000..ea41f6351 --- /dev/null +++ b/app/components/storeknox/discover/pending-review/index.scss @@ -0,0 +1,28 @@ +.review-header { + border: 1px solid var(--storeknox-discover-pending-review-header-border-color); + background-color: var(--storeknox-discover-pending-review-header-bg-color); + padding: 1em; +} + +.approve-button { + width: 7.714em; + background-color: var( + --storeknox-discover-pending-review-approve-button-color + ) !important; +} + +.reject-button { + width: 7.714em; + background-color: var( + --storeknox-discover-pending-review-reject-button-color + ) !important; +} + +.divider { + width: 1px; + height: 40px; + background-color: var( + --storeknox-discover-pending-review-header-divider-background-color + ); + margin: 0 0.35em; +} diff --git a/app/components/storeknox/discover/pending-review/index.ts b/app/components/storeknox/discover/pending-review/index.ts new file mode 100644 index 000000000..f0ad614fd --- /dev/null +++ b/app/components/storeknox/discover/pending-review/index.ts @@ -0,0 +1,130 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import IntlService from 'ember-intl/services/intl'; + +import { StoreknoxDiscoveryReviewQueryParam } from 'irene/routes/authenticated/storeknox/discover/review'; +import SkPendingReviewService from 'irene/services/sk-pending-review'; + +export interface StoreknoxDiscoverResultsSignature { + Args: { + queryParams: StoreknoxDiscoveryReviewQueryParam; + }; +} + +interface LimitOffset { + limit: number; + offset: number; +} + +export default class StoreknoxDiscoverPendingReviewComponent extends Component { + @service declare intl: IntlService; + @service declare skPendingReview: SkPendingReviewService; + + constructor(owner: unknown, args: StoreknoxDiscoverResultsSignature['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/discover/pending-review/table/found-by-header', + cellComponent: 'storeknox/discover/pending-review/table/found-by', + }, + // { + // headerComponent: + // 'storeknox/discover/pending-review/table/availability-header', + // cellComponent: 'storeknox/discover/pending-review/table/availability', + // textAlign: 'center', + // }, + { + name: this.intl.t('status'), + cellComponent: 'storeknox/discover/pending-review/table/status', + textAlign: 'center', + width: 80, + }, + ]; + } + + get reviewAppsData() { + if (this.skPendingReview.isFetchingData) { + return Array.from({ length: 5 }, () => ({})); + } else { + return this.skPendingReview.skPendingReviewData?.slice() || []; + } + } + + get loadingData() { + return this.skPendingReview.isFetchingData; + } + + // Table Actions + @action goToPage(args: LimitOffset) { + this.skPendingReview.fetchPendingReviewApps.perform( + args.limit, + args.offset + ); + } + + @action changePerPageItem(args: LimitOffset) { + this.skPendingReview.fetchPendingReviewApps.perform(args.limit, 0); + } + + @action approveMultipleApp() { + this.skPendingReview.approveRejectMultipleApp.perform(true); + } + + @action rejectMultipleApp() { + this.skPendingReview.approveRejectMultipleApp.perform(false); + } + + get limit() { + return Number(this.args.queryParams.app_limit); + } + + get offset() { + return Number(this.args.queryParams.app_offset); + } + + get totalCount() { + return this.skPendingReview.totalCount; + } + + get showButtons() { + return this.skPendingReview.selectedApps.length > 0; + } + + get showTable() { + return this.totalCount > 0 || this.loadingData; + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Storeknox::Discover::PendingReview': typeof StoreknoxDiscoverPendingReviewComponent; + } +} diff --git a/app/components/storeknox/discover/pending-review/table/availability-header/index.hbs b/app/components/storeknox/discover/pending-review/table/availability-header/index.hbs new file mode 100644 index 000000000..83d993366 --- /dev/null +++ b/app/components/storeknox/discover/pending-review/table/availability-header/index.hbs @@ -0,0 +1,83 @@ + + {{t 'storeknox.availability'}} + + + <:tooltipContent> +
+ + + + + {{t 'storeknox.info'}} + + + + + {{t 'storeknox.availableColumnInfo' htmlSafe=true}} + +
+ + + <:default> + + +
+ + +
+ + + + + {{t 'filterBy'}} + + + {{#each this.availabilityObject as |availability|}} + + + {{#if (eq this.selectedAvailability availability.value)}} + + {{else}} + + {{/if}} + + + {{availability.key}} + + {{/each}} + + {{#if this.filterApplied}} + + + {{t 'clearFilter'}} + + + {{/if}} + + \ No newline at end of file diff --git a/app/components/storeknox/discover/pending-review/table/availability-header/index.scss b/app/components/storeknox/discover/pending-review/table/availability-header/index.scss new file mode 100644 index 000000000..f88405b3b --- /dev/null +++ b/app/components/storeknox/discover/pending-review/table/availability-header/index.scss @@ -0,0 +1,41 @@ +.info-icon { + font-size: 1em !important; + height: 0.857em; + color: var( + --storeknox-discover-pending-review-table-availability-header-info-icon-color + ); +} + +.tooltip-content { + width: 17.857em; + padding: 0.5em; + box-sizing: border-box; + white-space: normal; +} + +.availability-filter { + width: 12.5em; + background-color: var( + --storeknox-discover-pending-review-table-availability-filter-background-color + ); + box-shadow: var( + --storeknox-discover-pending-review-table-availability-filter-box-shadow + ); + border-radius: 3px; + + .filter-option:hover { + background-color: var( + --storeknox-discover-pending-review-table-availability-filter-option-hover-background-color + ); + } + + .clear-filter-section { + background-color: var( + --storeknox-discover-pending-review-table-availability-filter-option-clear-filter-background-color + ); + } +} + +.cursor-pointer { + cursor: pointer; +} diff --git a/app/components/storeknox/discover/pending-review/table/availability-header/index.ts b/app/components/storeknox/discover/pending-review/table/availability-header/index.ts new file mode 100644 index 000000000..4375351f6 --- /dev/null +++ b/app/components/storeknox/discover/pending-review/table/availability-header/index.ts @@ -0,0 +1,72 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import IntlService from 'ember-intl/services/intl'; + +export default class StoreknoxDiscoverPendingReviewTableAvailabilityHeaderComponent extends Component { + @service declare intl: IntlService; + + @tracked anchorRef: HTMLElement | null = null; + @tracked selectedAvailability: number = -1; + @tracked filterApplied: boolean = false; + + @action + handleClick(event: FocusEvent) { + this.anchorRef = event.currentTarget as HTMLElement; + } + + @action + handleOptionsClose() { + this.anchorRef = null; + } + + @action + selectAvailability(value: number) { + this.selectedAvailability = value; + + if (value > -1) { + this.filterApplied = true; + } else { + this.filterApplied = false; + } + + this.anchorRef = null; + } + + @action + clearFilter() { + this.selectedAvailability = -1; + + this.filterApplied = false; + + this.anchorRef = null; + } + + get availabilityObject() { + return [ + { + key: this.intl.t('all'), + value: -1, + }, + { + key: this.intl.t('storeknox.vapt'), + value: 0, + }, + { + key: this.intl.t('appMonitoring'), + value: 1, + }, + { + key: this.intl.t('none'), + value: 2, + }, + ]; + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Storeknox::Discover::PendingReview::Table::AvailabilityHeader': typeof StoreknoxDiscoverPendingReviewTableAvailabilityHeaderComponent; + } +} diff --git a/app/components/storeknox/discover/pending-review/table/availability/index.hbs b/app/components/storeknox/discover/pending-review/table/availability/index.hbs new file mode 100644 index 000000000..7144c74a4 --- /dev/null +++ b/app/components/storeknox/discover/pending-review/table/availability/index.hbs @@ -0,0 +1,18 @@ +{{#if @data.available}} + {{#if (eq @data.available 'VAPT')}} + + {{else}} + + {{/if}} +{{else}} + +{{/if}} \ No newline at end of file diff --git a/app/components/storeknox/discover/pending-review/table/found-by-header/index.hbs b/app/components/storeknox/discover/pending-review/table/found-by-header/index.hbs new file mode 100644 index 000000000..75d2a6b2b --- /dev/null +++ b/app/components/storeknox/discover/pending-review/table/found-by-header/index.hbs @@ -0,0 +1,61 @@ + + {{t 'storeknox.foundBy'}} + + {{!-- --}} + + + + + + {{t 'filterBy'}} + + + {{#each this.discoveryObject as |discovery|}} + + + {{#if (eq this.selectedDiscovery discovery.value)}} + + {{else}} + + {{/if}} + + + {{discovery.key}} + + {{/each}} + + {{#if this.filterApplied}} + + + {{t 'clearFilter'}} + + + {{/if}} + + \ No newline at end of file diff --git a/app/components/storeknox/discover/pending-review/table/found-by-header/index.scss b/app/components/storeknox/discover/pending-review/table/found-by-header/index.scss new file mode 100644 index 000000000..8776077ea --- /dev/null +++ b/app/components/storeknox/discover/pending-review/table/found-by-header/index.scss @@ -0,0 +1,26 @@ +.found-by-filter { + width: 12.5em; + background-color: var( + --storeknox-discover-pending-review-table-found-by-filter-background-color + ); + box-shadow: var( + --storeknox-discover-pending-review-table-found-by-filter-box-shadow + ); + border-radius: 3px; + + .filter-option:hover { + background-color: var( + --storeknox-discover-pending-review-table-found-by-filter-option-hover-background-color + ); + } + + .clear-filter-section { + background-color: var( + --storeknox-discover-pending-review-table-found-by-filter-option-clear-filter-background-color + ); + } +} + +.cursor-pointer { + cursor: pointer; +} diff --git a/app/components/storeknox/discover/pending-review/table/found-by-header/index.ts b/app/components/storeknox/discover/pending-review/table/found-by-header/index.ts new file mode 100644 index 000000000..fabc2ea64 --- /dev/null +++ b/app/components/storeknox/discover/pending-review/table/found-by-header/index.ts @@ -0,0 +1,68 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import IntlService from 'ember-intl/services/intl'; + +export default class StoreknoxDiscoverPendingReviewTableFoundByHeaderComponent extends Component { + @service declare intl: IntlService; + + @tracked anchorRef: HTMLElement | null = null; + @tracked selectedDiscovery: number = -1; + @tracked filterApplied: boolean = false; + + @action + handleClick(event: FocusEvent) { + this.anchorRef = event.currentTarget as HTMLElement; + } + + @action + handleOptionsClose() { + this.anchorRef = null; + } + + @action + selectDiscovery(value: number) { + this.selectedDiscovery = value; + + if (value > -1) { + this.filterApplied = true; + } else { + this.filterApplied = false; + } + + this.anchorRef = null; + } + + @action + clearFilter() { + this.selectedDiscovery = -1; + + this.filterApplied = false; + + this.anchorRef = null; + } + + get discoveryObject() { + return [ + { + key: this.intl.t('all'), + value: -1, + }, + { + key: this.intl.t('storeknox.autoDiscovery'), + value: 0, + }, + { + key: this.intl.t('storeknox.manualDiscovery'), + value: 1, + }, + ]; + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Storeknox::Discover::PendingReview::Table::FoundByHeader': typeof StoreknoxDiscoverPendingReviewTableFoundByHeaderComponent; + } +} diff --git a/app/components/storeknox/discover/pending-review/table/found-by/index.hbs b/app/components/storeknox/discover/pending-review/table/found-by/index.hbs new file mode 100644 index 000000000..dc0e9c3c4 --- /dev/null +++ b/app/components/storeknox/discover/pending-review/table/found-by/index.hbs @@ -0,0 +1,27 @@ +{{#if @loading}} + +{{else}} + + + {{@data.foundBy}} + + + {{#if @data.addedBy}} + + <:tooltipContent> + + + + + {{@data.addedBy.email}} + + + + + <:default> + + + + {{/if}} + +{{/if}} \ No newline at end of file diff --git a/app/components/storeknox/discover/pending-review/table/found-by/index.scss b/app/components/storeknox/discover/pending-review/table/found-by/index.scss new file mode 100644 index 000000000..056fda764 --- /dev/null +++ b/app/components/storeknox/discover/pending-review/table/found-by/index.scss @@ -0,0 +1,7 @@ +.info-icon { + font-size: 1em !important; + height: 12px; + color: var( + --storeknox-discover-pending-review-table-found-by-info-icon-color + ); +} diff --git a/app/components/storeknox/discover/pending-review/table/index.hbs b/app/components/storeknox/discover/pending-review/table/index.hbs new file mode 100644 index 000000000..b44719694 --- /dev/null +++ b/app/components/storeknox/discover/pending-review/table/index.hbs @@ -0,0 +1,60 @@ + + + + + + {{#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/pending-review/table/index.scss b/app/components/storeknox/discover/pending-review/table/index.scss new file mode 100644 index 000000000..436f78f8e --- /dev/null +++ b/app/components/storeknox/discover/pending-review/table/index.scss @@ -0,0 +1,8 @@ +.review-table { + tr { + background-color: var(--storeknox-discover-pending-review-table-row-color); + border: 1px solid + var(--storeknox-discover-pending-review-table-row-border-color); + border-top: 0; + } +} diff --git a/app/components/storeknox/discover/pending-review/table/index.ts b/app/components/storeknox/discover/pending-review/table/index.ts new file mode 100644 index 000000000..f6fe8dd00 --- /dev/null +++ b/app/components/storeknox/discover/pending-review/table/index.ts @@ -0,0 +1,76 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import RouterService from '@ember/routing/router-service'; + +import SkAppModel from 'irene/models/sk-app'; +import SkPendingReviewService from 'irene/services/sk-pending-review'; + +interface LimitOffset { + limit: number; + offset: number; +} + +export interface StoreknoxDiscoverPendingReviewTableSignature { + Args: { + data: any; + limit: number; + offset: number; + loadingData: boolean; + totalCount: number; + goToPage: (args: LimitOffset) => void; + onItemPerPageChange: (args: LimitOffset) => void; + }; +} + +export default class StoreknoxDiscoverPendingReviewTableComponent extends Component { + @service declare router: RouterService; + @service declare skPendingReview: SkPendingReviewService; + + get tableData() { + return this.args.data; + } + + get limit() { + return this.args.limit; + } + + get offset() { + return this.args.offset; + } + + get loadingData() { + return this.args.loadingData; + } + + get totalCount() { + return this.args.totalCount; + } + + // Table Actions + @action goToPage(args: LimitOffset) { + this.args.goToPage(args); + } + + @action onItemPerPageChange(args: LimitOffset) { + this.args.onItemPerPageChange(args); + } + + @action selectRow(data: SkAppModel, value: boolean) { + this.skPendingReview.selectRow(data.id, value); + } + + get itemPerPageOptions() { + return [10, 25, 50]; + } + + @action selectAllRow(value: boolean) { + this.skPendingReview.selectAllRow(value); + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Storeknox::Discover::PendingReview::Table': typeof StoreknoxDiscoverPendingReviewTableComponent; + } +} diff --git a/app/components/storeknox/discover/pending-review/table/status/index.hbs b/app/components/storeknox/discover/pending-review/table/status/index.hbs new file mode 100644 index 000000000..37b4319c1 --- /dev/null +++ b/app/components/storeknox/discover/pending-review/table/status/index.hbs @@ -0,0 +1,85 @@ +{{#if (eq @data.status 'pending')}} + {{#if @loading}} + + + + {{else}} + {{#if @data.statusButtonsLoading}} + + {{else}} + + + + + + + + + + {{/if}} + {{/if}} +{{else if (eq @data.status 'approved')}} + + + {{t 'storeknox.approved'}} + + + + + {{t 'by'}} + {{@data.approvedBy.username}} + + + + <:tooltipContent> + + + + + {{day-js date=@data.approvedOn format='MMMM D, YYYY, HH:mm'}} + + + + + <:default> + + + + + +{{else if (eq @data.status 'rejected')}} + + + {{t 'rejected'}} + + + + + {{t 'by'}} + {{@data.actionTakenBy}} + + + + <:tooltipContent> + + + + + {{day-js date=@data.rejectedOn format='MMMM D, YYYY, HH:mm'}} + + + + + <:default> + + + + + +{{/if}} \ No newline at end of file diff --git a/app/components/storeknox/discover/pending-review/table/status/index.scss b/app/components/storeknox/discover/pending-review/table/status/index.scss new file mode 100644 index 000000000..98cf7531b --- /dev/null +++ b/app/components/storeknox/discover/pending-review/table/status/index.scss @@ -0,0 +1,5 @@ +.info-icon { + font-size: 1em !important; + height: 12px; + color: var(--storeknox-discover-pending-review-table-status-info-icon-color); +} diff --git a/app/components/storeknox/discover/pending-review/table/status/index.ts b/app/components/storeknox/discover/pending-review/table/status/index.ts new file mode 100644 index 000000000..403b5113f --- /dev/null +++ b/app/components/storeknox/discover/pending-review/table/status/index.ts @@ -0,0 +1,37 @@ +import Component from '@glimmer/component'; +import Store from '@ember-data/store'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; + +import MeService from 'irene/services/me'; +import SkAppModel from 'irene/models/sk-app'; +import SkPendingReviewService from 'irene/services/sk-pending-review'; + +interface StoreknoxDiscoverPendingReviewTableStatusSignature { + Args: { + data: SkAppModel; + loading: boolean; + }; +} + +export default class StoreknoxDiscoverPendingReviewTableStatusComponent extends Component { + @service declare me: MeService; + @service declare store: Store; + @service declare skPendingReview: SkPendingReviewService; + + @action + approveApp(id: string) { + this.skPendingReview.approveRejectApp.perform(id, true); + } + + @action + rejectApp(id: string) { + this.skPendingReview.approveRejectApp.perform(id, false); + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Storeknox::Discover::PendingReview::Table::Status': typeof StoreknoxDiscoverPendingReviewTableStatusComponent; + } +} diff --git a/app/components/storeknox/discover/requested-apps/index.hbs b/app/components/storeknox/discover/requested-apps/index.hbs new file mode 100644 index 000000000..d8b097dfa --- /dev/null +++ b/app/components/storeknox/discover/requested-apps/index.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/components/storeknox/discover/requested-apps/index.ts b/app/components/storeknox/discover/requested-apps/index.ts new file mode 100644 index 000000000..0ede3a6e1 --- /dev/null +++ b/app/components/storeknox/discover/requested-apps/index.ts @@ -0,0 +1,16 @@ +import Component from '@glimmer/component'; +import { StoreknoxDiscoveryRequestedQueryParam } from 'irene/routes/authenticated/storeknox/discover/requested'; + +export interface StoreknoxDiscoverRequestedAppsSignature { + Args: { + queryParams: StoreknoxDiscoveryRequestedQueryParam; + }; +} + +export default class StoreknoxDiscoverRequestedAppsComponent extends Component {} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Storeknox::Discover::RequestedApps': typeof StoreknoxDiscoverRequestedAppsComponent; + } +} diff --git a/app/components/storeknox/discover/requested-apps/table/index.hbs b/app/components/storeknox/discover/requested-apps/table/index.hbs new file mode 100644 index 000000000..542519171 --- /dev/null +++ b/app/components/storeknox/discover/requested-apps/table/index.hbs @@ -0,0 +1,70 @@ +{{#if this.hasNoApps}} + + + + + {{t 'storeknox.noRequestedAppsFound'}} + + + + {{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}} + + + + + + + +{{/if}} \ 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 new file mode 100644 index 000000000..78f51927f --- /dev/null +++ b/app/components/storeknox/discover/requested-apps/table/index.scss @@ -0,0 +1,26 @@ +.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; + } +} + +.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/discover/requested-apps/table/index.ts b/app/components/storeknox/discover/requested-apps/table/index.ts new file mode 100644 index 000000000..51f4ab8a0 --- /dev/null +++ b/app/components/storeknox/discover/requested-apps/table/index.ts @@ -0,0 +1,108 @@ +import { action } from '@ember/object'; +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import RouterService from '@ember/routing/router-service'; +import IntlService from 'ember-intl/services/intl'; + +import { StoreknoxDiscoveryRequestedQueryParam } from 'irene/routes/authenticated/storeknox/discover/requested'; +import SkRequestedAppService from 'irene/services/sk-requested-app'; + +interface LimitOffset { + limit: number; + offset: number; +} + +export interface StoreknoxDiscoverRequestedAppsTableSignature { + Args: { + queryParams: StoreknoxDiscoveryRequestedQueryParam; + }; +} + +export default class StoreknoxDiscoverRequestedAppsTableComponent extends Component { + @service declare router: RouterService; + @service declare intl: IntlService; + @service declare skRequestedApp: SkRequestedAppService; + + constructor( + owner: unknown, + args: StoreknoxDiscoverRequestedAppsTableSignature['Args'] + ) { + super(owner, args); + + const { app_limit, app_offset } = args.queryParams; + + this.skRequestedApp.fetchRequestedApps.perform(app_limit, app_offset); + } + + // Table Actions + @action goToPage(args: LimitOffset) { + this.skRequestedApp.fetchRequestedApps.perform(args.limit, args.offset); + } + + @action onItemPerPageChange(args: LimitOffset) { + this.skRequestedApp.fetchRequestedApps.perform(args.limit, 0); + } + + get totalCount() { + return this.skRequestedApp.totalCount; + } + + get hasNoApps() { + return this.skRequestedApp.totalCount <= 0 && !this.loadingData; + } + + get requestedAppsData() { + if (this.skRequestedApp.isFetchingData) { + return Array.from({ length: 5 }, () => ({})); + } else { + return this.skRequestedApp.skRequestedAppData?.slice() || []; + } + } + + get itemPerPageOptions() { + return [10, 25, 50]; + } + + get limit() { + return Number(this.args.queryParams.app_limit); + } + + get offset() { + return Number(this.args.queryParams.app_offset); + } + + get loadingData() { + return this.skRequestedApp.isFetchingData; + } + + 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 new file mode 100644 index 000000000..679429c10 --- /dev/null +++ b/app/components/storeknox/discover/requested-apps/table/status/index.hbs @@ -0,0 +1,69 @@ +{{#if (eq @data.status 'pending')}} + + <:icon> + + + +{{else if (eq @data.status 'approved')}} + + + {{t 'storeknox.approved'}} + + + + + {{t 'by'}} + {{@data.approvedBy}} + + + + <:tooltipContent> + + + + + {{day-js date=@data.approvedOn format='MMMM D, YYYY, HH:mm'}} + + + + + <:default> + + + + + +{{else if (eq @data.status 'rejected')}} + + + {{t 'rejected'}} + + + + + {{t 'by'}} + {{@data.rejectedBy}} + + + + <:tooltipContent> + + + + + {{day-js date=@data.rejectedOn format='MMMM D, YYYY, HH:mm'}} + + + + + <: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 new file mode 100644 index 000000000..761831828 --- /dev/null +++ b/app/components/storeknox/discover/requested-apps/table/status/index.scss @@ -0,0 +1,5 @@ +.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/discover/results/empty/index.hbs b/app/components/storeknox/discover/results/empty/index.hbs new file mode 100644 index 000000000..3f47e6317 --- /dev/null +++ b/app/components/storeknox/discover/results/empty/index.hbs @@ -0,0 +1,12 @@ + + + + + + {{t 'storeknox.searchForApps'}} + + + + {{t 'storeknox.searchForAppsDescription'}} + + \ No newline at end of file diff --git a/app/components/storeknox/discover/results/empty/index.scss b/app/components/storeknox/discover/results/empty/index.scss new file mode 100644 index 000000000..4488f8286 --- /dev/null +++ b/app/components/storeknox/discover/results/empty/index.scss @@ -0,0 +1,13 @@ +.empty-container { + margin-top: 3em; + margin-bottom: 3em; + + .header { + margin-top: 1.5625em; + } + + .body-text { + text-align: center; + max-width: 500px; + } +} diff --git a/app/components/storeknox/discover/results/empty/index.ts b/app/components/storeknox/discover/results/empty/index.ts new file mode 100644 index 000000000..abed71cd3 --- /dev/null +++ b/app/components/storeknox/discover/results/empty/index.ts @@ -0,0 +1,9 @@ +import Component from '@glimmer/component'; + +export default class StoreknoxDiscoverResultsEmptyComponent extends Component {} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Storeknox::Discover::Results::Empty': typeof StoreknoxDiscoverResultsEmptyComponent; + } +} diff --git a/app/components/storeknox/discover/results/index.hbs b/app/components/storeknox/discover/results/index.hbs new file mode 100644 index 000000000..06839dd74 --- /dev/null +++ b/app/components/storeknox/discover/results/index.hbs @@ -0,0 +1,34 @@ +
+ +
+ + <:rightAdornment> + {{#if this.searchQuery}} + + + + {{else}} + + {{/if}} + + +
+ + + {{t 'storeknox.discoverHeader'}} + +
+
+ +{{#if this.discoverClicked}} + +{{else}} + +{{/if}} \ No newline at end of file diff --git a/app/components/storeknox/discover/results/index.scss b/app/components/storeknox/discover/results/index.scss new file mode 100644 index 000000000..c505a0df5 --- /dev/null +++ b/app/components/storeknox/discover/results/index.scss @@ -0,0 +1,9 @@ +.search-input-container-width { + width: 530px; + margin-right: 0.714em; + background-color: var(--storeknox-discover-results-input-bg-color); + + .close-search-text { + cursor: pointer; + } +} diff --git a/app/components/storeknox/discover/results/index.ts b/app/components/storeknox/discover/results/index.ts new file mode 100644 index 000000000..42ab97b56 --- /dev/null +++ b/app/components/storeknox/discover/results/index.ts @@ -0,0 +1,75 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import RouterService from '@ember/routing/router-service'; +import type Store from '@ember-data/store'; +import IntlService from 'ember-intl/services/intl'; + +import { StoreknoxDiscoveryResultQueryParam } from 'irene/routes/authenticated/storeknox/discover/result'; +import SkDiscoveryResultService from 'irene/services/sk-discovery-result'; + +export interface StoreknoxDiscoverResultsSignature { + Args: { + queryParams: StoreknoxDiscoveryResultQueryParam; + }; +} + +export default class StoreknoxDiscoverResultsComponent extends Component { + @service declare store: Store; + @service declare router: RouterService; + @service declare skDiscoveryResult: SkDiscoveryResultService; + @service('notifications') declare notify: NotificationService; + @service declare intl: IntlService; + + @tracked searchQuery = ''; + @tracked discoverClicked = false; + + constructor(owner: unknown, args: StoreknoxDiscoverResultsSignature['Args']) { + super(owner, args); + + const { app_query } = args.queryParams; + + if (app_query) { + this.discoverClicked = true; + + this.searchQuery = app_query; + } + } + @action + async discoverApp(event: SubmitEvent) { + event.preventDefault(); + + if (this.searchQuery.length > 1) { + this.router.transitionTo('authenticated.storeknox.discover.result', { + queryParams: { app_query: this.searchQuery, app_search_id: null }, + }); + + if (this.discoverClicked) { + const { app_limit } = this.args.queryParams; + + this.skDiscoveryResult.uploadSearchQuery.perform( + this.searchQuery, + app_limit, + 0, + true + ); + } else { + this.discoverClicked = true; + } + } else { + this.notify.error(this.intl.t('storeknox.errorSearchCharacter')); + } + } + + @action + clearSearch() { + this.searchQuery = ''; + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Storeknox::Discover::Results': typeof StoreknoxDiscoverResultsComponent; + } +} diff --git a/app/components/storeknox/discover/results/table/action-header/index.hbs b/app/components/storeknox/discover/results/table/action-header/index.hbs new file mode 100644 index 000000000..d8fd86a9d --- /dev/null +++ b/app/components/storeknox/discover/results/table/action-header/index.hbs @@ -0,0 +1,18 @@ + + {{t 'action'}} + + + + <:tooltipContent> +
+ + {{this.tooltipText}} + +
+ + + <:default> + + +
+
\ No newline at end of file diff --git a/app/components/storeknox/discover/results/table/action-header/index.scss b/app/components/storeknox/discover/results/table/action-header/index.scss new file mode 100644 index 000000000..699453d2e --- /dev/null +++ b/app/components/storeknox/discover/results/table/action-header/index.scss @@ -0,0 +1,6 @@ +.tooltip-content { + width: 14.285em; + padding: 0.5em; + box-sizing: border-box; + white-space: normal; +} diff --git a/app/components/storeknox/discover/results/table/action-header/index.ts b/app/components/storeknox/discover/results/table/action-header/index.ts new file mode 100644 index 000000000..d20485e10 --- /dev/null +++ b/app/components/storeknox/discover/results/table/action-header/index.ts @@ -0,0 +1,35 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import IntlService from 'ember-intl/services/intl'; + +import MeService from 'irene/services/me'; +import SkDiscoverySearchResultModel from 'irene/models/sk-discovery-result'; + +interface StoreknoxDiscoverResultsTableActionHeaderSignature { + Args: { + data: SkDiscoverySearchResultModel; + }; +} + +export default class StoreknoxDiscoverResultsTableActionHeaderComponent extends Component { + @service declare me: MeService; + @service declare intl: IntlService; + + get isAdmin() { + return this.me.org?.is_admin; + } + + get tooltipText() { + if (this.isAdmin) { + return this.intl.t('storeknox.actionHeaderInfoOwner'); + } + + return this.intl.t('storeknox.actionHeaderInfo'); + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Storeknox::Discover::Results::Table::ActionHeader': typeof StoreknoxDiscoverResultsTableActionHeaderComponent; + } +} diff --git a/app/components/storeknox/discover/results/table/action/index.hbs b/app/components/storeknox/discover/results/table/action/index.hbs new file mode 100644 index 000000000..fcfde2dd6 --- /dev/null +++ b/app/components/storeknox/discover/results/table/action/index.hbs @@ -0,0 +1,57 @@ +{{#if @data.requested}} + {{#if @data.approved}} + + <:tooltipContent> +
+ + {{t 'storeknox.appAlreadyExists'}} + +
+ + + <:default> + + +
+ {{else}} + + <:tooltipContent> +
+ + {{t 'storeknox.appAlreadyRequested'}} + +
+ + + <:default> + + +
+ {{/if}} +{{else}} + {{#if @loading}} + + + + {{else}} + {{#if @data.addButtonLoading}} + + {{else}} + + {{#if this.isAdmin}} + + {{else}} + + {{/if}} + + {{/if}} + {{/if}} +{{/if}} \ No newline at end of file diff --git a/app/components/storeknox/discover/results/table/action/index.scss b/app/components/storeknox/discover/results/table/action/index.scss new file mode 100644 index 000000000..43497d98f --- /dev/null +++ b/app/components/storeknox/discover/results/table/action/index.scss @@ -0,0 +1,26 @@ +.requested-icon { + padding: 0.4em; + color: var(--storeknox-discover-results-table-action-requested-icon-color); + background-color: var( + --storeknox-discover-results-table-action-requested-icon-bgcolor + ); + border-radius: 2px; +} + +.already-exist-icon { + padding: 0.4em; + color: var( + --storeknox-discover-results-table-action-already-exist-icon-color + ); + background-color: var( + --storeknox-discover-results-table-action-already-exist-icon-bgcolor + ); + border-radius: 2px; +} + +.tooltip-content { + width: 12.85em; + padding: 0.5em; + box-sizing: border-box; + white-space: normal; +} diff --git a/app/components/storeknox/discover/results/table/action/index.ts b/app/components/storeknox/discover/results/table/action/index.ts new file mode 100644 index 000000000..bb10483dd --- /dev/null +++ b/app/components/storeknox/discover/results/table/action/index.ts @@ -0,0 +1,36 @@ +import Component from '@glimmer/component'; +import Store from '@ember-data/store'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; + +import MeService from 'irene/services/me'; +import SkDiscoverySearchResultModel from 'irene/models/sk-discovery-result'; +import SkDiscoveryResultService from 'irene/services/sk-discovery-result'; + +interface StoreknoxDiscoverResultsTableActionSignature { + Args: { + data: SkDiscoverySearchResultModel; + loading: boolean; + }; +} + +export default class StoreknoxDiscoverResultsTableActionComponent extends Component { + @service declare me: MeService; + @service declare store: Store; + @service declare skDiscoveryResult: SkDiscoveryResultService; + + get isAdmin() { + return this.me.org?.is_admin; + } + + @action + addToInventory(ulid: string) { + this.skDiscoveryResult.addToInventory.perform(ulid); + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Storeknox::Discover::Results::Table::Action': typeof StoreknoxDiscoverResultsTableActionComponent; + } +} diff --git a/app/components/storeknox/discover/results/table/index.hbs b/app/components/storeknox/discover/results/table/index.hbs new file mode 100644 index 000000000..2474c623e --- /dev/null +++ b/app/components/storeknox/discover/results/table/index.hbs @@ -0,0 +1,117 @@ +{{#if this.loadingData}} + + + + {{! }} + +{{else}} + + + + {{t 'storeknox.showingResults'}} + + + +  "{{@queryParams.app_query}}" + + + + {{!-- + <:leftIcon> + + + + <:default> + {{this.buttonText}} + + --}} + +{{/if}} + +{{#if this.hasNoApps}} + + + + + {{t 'storeknox.noResultFound'}} + + + + {{t 'storeknox.noResultFoundDescription'}} + + +{{else}} + + + + + + {{#if column.headerComponent}} + {{#let (component column.headerComponent) as |Component|}} + + {{/let}} + {{else}} + {{column.name}} + {{/if}} + + + + + + + + {{#let (component r.columnValue.cellComponent) as |Component|}} + + {{/let}} + + + + + + + +{{/if}} \ No newline at end of file diff --git a/app/components/storeknox/discover/results/table/index.scss b/app/components/storeknox/discover/results/table/index.scss new file mode 100644 index 000000000..330b09d30 --- /dev/null +++ b/app/components/storeknox/discover/results/table/index.scss @@ -0,0 +1,36 @@ +.result-header { + border: 1px solid var(--storeknox-discover-results-table-header-border-color); + padding: 1em; + margin-top: 2.14em; + background-color: var(--storeknox-discover-results-table-header-bg-color); +} + +.results-back-icon { + font-size: 1.714em !important; + margin-right: 0.214em; +} + +.results-table { + tr { + background-color: var(--storeknox-discover-results-table-row-color); + border: 1px solid var(--storeknox-discover-results-table-row-border-color); + border-top: 0; + } +} + +.empty-container { + padding-top: 5em; + padding-bottom: 5em; + border: 1px solid var(--storeknox-discover-results-table-header-border-color); + background-color: var(--storeknox-discover-results-table-header-bg-color); + border-top: 0; + + .header { + margin-top: 1.5625em; + } + + .body-text { + text-align: center; + max-width: 550px; + } +} diff --git a/app/components/storeknox/discover/results/table/index.ts b/app/components/storeknox/discover/results/table/index.ts new file mode 100644 index 000000000..54b3bc979 --- /dev/null +++ b/app/components/storeknox/discover/results/table/index.ts @@ -0,0 +1,186 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import RouterService from '@ember/routing/router-service'; +import IntlService from 'ember-intl/services/intl'; +import Store from '@ember-data/store'; + +import MeService from 'irene/services/me'; +import { StoreknoxDiscoveryResultQueryParam } from 'irene/routes/authenticated/storeknox/discover/result'; +import SkDiscoveryResultService from 'irene/services/sk-discovery-result'; +import SkDiscoverySearchResultModel from 'irene/models/sk-discovery-result'; + +interface LimitOffset { + limit: number; + offset: number; +} + +export interface StoreknoxDiscoverResultsTableSignature { + Args: { + queryParams: StoreknoxDiscoveryResultQueryParam; + }; +} + +export default class StoreknoxDiscoverResultsTableComponent extends Component { + @service declare router: RouterService; + @service declare intl: IntlService; + @service declare me: MeService; + @service declare skDiscoveryResult: SkDiscoveryResultService; + @service declare store: Store; + + constructor( + owner: unknown, + args: StoreknoxDiscoverResultsTableSignature['Args'] + ) { + super(owner, args); + + const { app_limit, app_offset, app_search_id, app_query } = + args.queryParams; + + if (app_search_id) { + this.skDiscoveryResult.fetchDiscoveryResults.perform( + app_limit, + app_offset, + app_search_id, + false + ); + } else { + this.skDiscoveryResult.uploadSearchQuery.perform( + app_query, + app_limit, + app_offset + ); + } + } + + get searchResultsData() { + if (this.skDiscoveryResult.isFetchingData) { + return Array.from({ length: 5 }, () => ({})); + } else { + return this.skDiscoveryResult.skDiscoveryResultData?.slice() || []; + } + } + + get loadingData() { + return this.skDiscoveryResult.isFetchingData; + } + + 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, + }, + { + name: this.intl.t('developer'), + cellComponent: 'storeknox/table-columns/developer', + width: 200, + }, + { + headerComponent: 'storeknox/discover/results/table/action-header', + cellComponent: 'storeknox/discover/results/table/action', + minWidth: 50, + width: 50, + textAlign: 'center', + }, + ]; + } + + // Table Actions + @action goToPage(args: LimitOffset) { + const { app_search_id } = this.args.queryParams; + + this.skDiscoveryResult.fetchDiscoveryResults.perform( + args.limit, + args.offset, + app_search_id + ); + } + + @action onItemPerPageChange(args: LimitOffset) { + const { app_search_id } = this.args.queryParams; + + this.skDiscoveryResult.fetchDiscoveryResults.perform( + args.limit, + 0, + app_search_id + ); + } + + @action selectRow(data: SkDiscoverySearchResultModel, value: boolean) { + this.skDiscoveryResult.selectRow(data.docUlid, value); + } + + @action sendRequest() { + this.skDiscoveryResult.addMultipleToInventory.perform(); + } + + @action selectAllRow(value: boolean) { + this.skDiscoveryResult.selectAllRow(value); + } + + get disabledButton() { + return this.skDiscoveryResult.selectedResults.length === 0; + } + + get totalCount() { + return this.skDiscoveryResult.skDiscoveryResultData?.meta?.count || 0; + } + + get hasNoApps() { + const appCount = this.skDiscoveryResult.skDiscoveryResultData?.meta?.count; + + if (appCount) { + return appCount <= 0; + } + + return true; + } + + get itemPerPageOptions() { + return [10, 25, 50]; + } + + get limit() { + return Number(this.args.queryParams.app_limit); + } + + get offset() { + return Number(this.args.queryParams.app_offset); + } + + get isAdmin() { + return this.me.org?.is_admin; + } + + get buttonIconName() { + return this.isAdmin ? 'add-box' : 'send'; + } + + get buttonText() { + return this.isAdmin + ? this.intl.t('storeknox.addToInventory') + : this.intl.t('storeknox.sendRequest'); + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Storeknox::Discover::Results::Table': typeof StoreknoxDiscoverResultsTableComponent; + } +} diff --git a/app/components/storeknox/discover/welcome-modal/index.hbs b/app/components/storeknox/discover/welcome-modal/index.hbs new file mode 100644 index 000000000..b626496cc --- /dev/null +++ b/app/components/storeknox/discover/welcome-modal/index.hbs @@ -0,0 +1,49 @@ + + + + + + + + + + + + We have detected + 15 apps + that may belong to your organization which exists on Google Play Store + and Apple App Store. + + + + + + + + In addition, there are + 30 apps + that are present in your Organizations Appknox account which should also + be added to Storeknox. + + + + + + + + You can use the button below to review them and take necessary action. + + + + + + + +
+ + + Take Action + + +
+
\ No newline at end of file diff --git a/app/components/storeknox/discover/welcome-modal/index.scss b/app/components/storeknox/discover/welcome-modal/index.scss new file mode 100644 index 000000000..d4f00ee4b --- /dev/null +++ b/app/components/storeknox/discover/welcome-modal/index.scss @@ -0,0 +1,12 @@ +.modal-body { + padding: 15px 35px; + + .modal-body-list { + padding: 15px 18px; + } +} + +.modal-footer { + box-shadow: 0px -1px 10px 0px #0000000d; + padding: 10px 0; +} diff --git a/app/components/storeknox/discover/welcome-modal/index.ts b/app/components/storeknox/discover/welcome-modal/index.ts new file mode 100644 index 000000000..c373d77cd --- /dev/null +++ b/app/components/storeknox/discover/welcome-modal/index.ts @@ -0,0 +1,9 @@ +import Component from '@glimmer/component'; + +export default class StoreknoxDiscoverWelcomeModalComponent extends Component {} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Storeknox::Discover::WelcomeModal': typeof StoreknoxDiscoverWelcomeModalComponent; + } +} diff --git a/app/components/storeknox/indicator/index.hbs b/app/components/storeknox/indicator/index.hbs new file mode 100644 index 000000000..b39ec8168 --- /dev/null +++ b/app/components/storeknox/indicator/index.hbs @@ -0,0 +1,21 @@ + + <:tooltipContent> +
+ + + + + {{t 'storeknox.info'}} + + + + + {{@text}} + +
+ + + <:default> + {{component @svgComponent}} + +
\ No newline at end of file diff --git a/app/components/storeknox/indicator/index.scss b/app/components/storeknox/indicator/index.scss new file mode 100644 index 000000000..aeb19ed1d --- /dev/null +++ b/app/components/storeknox/indicator/index.scss @@ -0,0 +1,7 @@ +.tooltip-content { + width: 200px; + max-width: max-content; + padding: 0.5em; + box-sizing: border-box; + white-space: normal; +} diff --git a/app/components/storeknox/indicator/index.ts b/app/components/storeknox/indicator/index.ts new file mode 100644 index 000000000..6ea2f2b33 --- /dev/null +++ b/app/components/storeknox/indicator/index.ts @@ -0,0 +1,9 @@ +import Component from '@glimmer/component'; + +export default class StoreknoxIndicatorComponent extends Component {} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Storeknox::Indicator': typeof StoreknoxIndicatorComponent; + } +} diff --git a/app/components/storeknox/review-logs/index.hbs b/app/components/storeknox/review-logs/index.hbs new file mode 100644 index 000000000..f3079dcc2 --- /dev/null +++ b/app/components/storeknox/review-logs/index.hbs @@ -0,0 +1,33 @@ + + {{#each this.breadcrumbItems as |item|}} + + {{/each}} + + + + + {{t 'storeknox.reviewLogs'}} + + + + Lorem ipsum dolor sit amet consectetur. At suspendisse et orci risus non + sed. Mauris dui tincidunt + + + + \ No newline at end of file diff --git a/app/components/storeknox/review-logs/index.scss b/app/components/storeknox/review-logs/index.scss new file mode 100644 index 000000000..c1e2c76dc --- /dev/null +++ b/app/components/storeknox/review-logs/index.scss @@ -0,0 +1,11 @@ +.header-storeknox-review-logs-page { + margin-top: 0.714em; + border: 1px solid var(--storeknox-review-logs-header-border-color); + background-color: var(--storeknox-review-logs-header-background-color); + padding: 1.428em; + margin-bottom: 2.14em; +} + +.description-storeknox-review-logs-page { + color: var(--storeknox-review-logs-header-description-color); +} diff --git a/app/components/storeknox/review-logs/index.ts b/app/components/storeknox/review-logs/index.ts new file mode 100644 index 000000000..882e6684f --- /dev/null +++ b/app/components/storeknox/review-logs/index.ts @@ -0,0 +1,88 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import IntlService from 'ember-intl/services/intl'; + +export default class StoreknoxReviewLogsComponent extends Component { + @service declare intl: IntlService; + + get breadcrumbItems() { + return [ + { + route: 'authenticated.storeknox.discover.review', + linkTitle: 'Home', + }, + { + route: 'authenticated.storeknox.review-logs', + linkTitle: 'Review Logs', + }, + ]; + } + + get columns() { + return [ + { + 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/discover/pending-review/table/found-by-header', + cellComponent: 'storeknox/discover/pending-review/table/found-by', + }, + { + name: this.intl.t('status'), + cellComponent: 'storeknox/discover/pending-review/table/status', + width: 80, + }, + ]; + } + + get reviewLogApps() { + return [ + { + isAndroid: true, + iconUrl: + 'https://appknox-production-public.s3.amazonaws.com/908e507e-1148-4f4d-9939-6dba3d645abc.png', + name: 'Shell Asia', + packageName: 'com.shellasia.android', + foundBy: 'Auto Discovery', + status: 'approved', + actionTakenBy: 'sujith', + }, + { + isIos: true, + iconUrl: + 'https://appknox-production-public.s3.amazonaws.com/908e507e-1148-4f4d-9939-6dba3d645abc.png', + name: 'Shell Recharge India', + packageName: 'com.shellrecharge.india', + foundBy: 'Manual Discovery', + status: 'approved', + actionTakenBy: 'smit', + }, + { + isAndroid: true, + iconUrl: + 'https://appknox-production-public.s3.amazonaws.com/908e507e-1148-4f4d-9939-6dba3d645abc.png', + name: 'Shell Mobility Site Manager', + packageName: 'com.shellmobility.ios', + foundBy: 'Auto Discovery', + status: 'rejected', + actionTakenBy: 'sujith', + }, + ]; + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Storeknox::ReviewLogs': typeof StoreknoxReviewLogsComponent; + } +} diff --git a/app/components/storeknox/table-columns/application/index.hbs b/app/components/storeknox/table-columns/application/index.hbs new file mode 100644 index 000000000..173034858 --- /dev/null +++ b/app/components/storeknox/table-columns/application/index.hbs @@ -0,0 +1,48 @@ + + {{#if @loading}} + + + + + + + + + + {{else}} + + + + + + {{@data.title}} + + + + + {{@data.packageName}} + + + {{/if}} + \ No newline at end of file diff --git a/app/components/storeknox/table-columns/checkbox-header/index.hbs b/app/components/storeknox/table-columns/checkbox-header/index.hbs new file mode 100644 index 000000000..ae6fe4043 --- /dev/null +++ b/app/components/storeknox/table-columns/checkbox-header/index.hbs @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/app/components/storeknox/table-columns/checkbox-header/index.ts b/app/components/storeknox/table-columns/checkbox-header/index.ts new file mode 100644 index 000000000..f84031837 --- /dev/null +++ b/app/components/storeknox/table-columns/checkbox-header/index.ts @@ -0,0 +1,22 @@ +import { action } from '@ember/object'; +import Component from '@glimmer/component'; + +interface StoreknoxDiscoverTableColumnsCheckboxHeaderSignature { + Args: { + loading: boolean; + selected: boolean; + selectAllRow: (value: boolean) => void; + }; +} + +export default class StoreknoxDiscoverTableColumnsCheckboxHeaderComponent extends Component { + @action handleChange(event: Event) { + this.args.selectAllRow((event.target as HTMLInputElement).checked); + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Storeknox::Discover::TableColumns::CheckboxHeader': typeof StoreknoxDiscoverTableColumnsCheckboxHeaderComponent; + } +} diff --git a/app/components/storeknox/table-columns/checkbox/index.hbs b/app/components/storeknox/table-columns/checkbox/index.hbs new file mode 100644 index 000000000..4ded42411 --- /dev/null +++ b/app/components/storeknox/table-columns/checkbox/index.hbs @@ -0,0 +1,11 @@ +{{#if @loading}} + + + +{{else}} + +{{/if}} \ No newline at end of file diff --git a/app/components/storeknox/table-columns/checkbox/index.ts b/app/components/storeknox/table-columns/checkbox/index.ts new file mode 100644 index 000000000..3e97f5ce2 --- /dev/null +++ b/app/components/storeknox/table-columns/checkbox/index.ts @@ -0,0 +1,24 @@ +import { action } from '@ember/object'; +import Component from '@glimmer/component'; + +import SkDiscoverySearchResultModel from 'irene/models/sk-discovery-result'; + +interface StoreknoxDiscoverTableColumnsCheckboxSignature { + Args: { + data: SkDiscoverySearchResultModel; + loading: boolean; + selectRow: (value: boolean) => void; + }; +} + +export default class StoreknoxDiscoverTableColumnsCheckboxComponent extends Component { + @action handleChange(event: Event) { + this.args.selectRow((event.target as HTMLInputElement).checked); + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Storeknox::Discover::TableColumns::Checkbox': typeof StoreknoxDiscoverTableColumnsCheckboxComponent; + } +} diff --git a/app/components/storeknox/table-columns/developer/index.hbs b/app/components/storeknox/table-columns/developer/index.hbs new file mode 100644 index 000000000..40e7fdc20 --- /dev/null +++ b/app/components/storeknox/table-columns/developer/index.hbs @@ -0,0 +1,28 @@ +{{#if @loading}} + + + + + +{{else}} + + + {{@data.devName}} + + + {{#if @data.devEmail}} + + {{@data.devEmail}} + + {{else}} + + {{t 'noDataAvailable'}} + + {{/if}} + +{{/if}} \ No newline at end of file diff --git a/app/components/storeknox/table-columns/index.ts b/app/components/storeknox/table-columns/index.ts new file mode 100644 index 000000000..d14e001d1 --- /dev/null +++ b/app/components/storeknox/table-columns/index.ts @@ -0,0 +1,9 @@ +import Component from '@glimmer/component'; + +export default class StoreknoxDiscoverTableColumnsComponent extends Component {} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Storeknox::Discover::TableColumns': typeof StoreknoxDiscoverTableColumnsComponent; + } +} diff --git a/app/components/storeknox/table-columns/store-header/index.hbs b/app/components/storeknox/table-columns/store-header/index.hbs new file mode 100644 index 000000000..24e838fd5 --- /dev/null +++ b/app/components/storeknox/table-columns/store-header/index.hbs @@ -0,0 +1,59 @@ + + {{t 'storeknox.store'}} + + {{!-- --}} + + + + + {{t 'filterBy'}} + + {{#each this.platformObject as |platform|}} + + + {{#if (eq this.selectedPlatform platform.value)}} + + {{else}} + + {{/if}} + + + {{platform.key}} + + {{/each}} + + {{#if this.filterApplied}} + + + {{t 'clearFilter'}} + + + {{/if}} + + \ No newline at end of file diff --git a/app/components/storeknox/table-columns/store-header/index.scss b/app/components/storeknox/table-columns/store-header/index.scss new file mode 100644 index 000000000..2d1e50fcd --- /dev/null +++ b/app/components/storeknox/table-columns/store-header/index.scss @@ -0,0 +1,24 @@ +.store-filter { + width: 175px; + background-color: var( + --storeknox-table-columns-store-header-filter-background-color + ); + box-shadow: var(--storeknox-table-columns-store-header-filter-box-shadow); + border-radius: 3px; + + .filter-option:hover { + background-color: var( + --storeknox-table-columns-store-header-filter-option-hover-bgcolor + ); + } + + .clear-filter-section { + background-color: var( + --storeknox-table-columns-store-header-filter-option-clear-filter-bgcolor + ); + } +} + +.cursor-pointer { + cursor: pointer; +} diff --git a/app/components/storeknox/table-columns/store-header/index.ts b/app/components/storeknox/table-columns/store-header/index.ts new file mode 100644 index 000000000..1c205f7b0 --- /dev/null +++ b/app/components/storeknox/table-columns/store-header/index.ts @@ -0,0 +1,70 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import IntlService from 'ember-intl/services/intl'; + +import ENUMS from 'irene/enums'; + +export default class StoreknoxDiscoverTableColumnsStoreHeaderComponent extends Component { + @service declare intl: IntlService; + + @tracked anchorRef: HTMLElement | null = null; + @tracked selectedPlatform: number = -1; + @tracked filterApplied: boolean = false; + + @action + handleClick(event: FocusEvent) { + this.anchorRef = event.currentTarget as HTMLElement; + } + + @action + handleOptionsClose() { + this.anchorRef = null; + } + + @action + selectPlatform(value: number) { + this.selectedPlatform = value; + + if (value > -1) { + this.filterApplied = true; + } else { + this.filterApplied = false; + } + + this.anchorRef = null; + } + + @action + clearFilter() { + this.selectedPlatform = -1; + + this.filterApplied = false; + + this.anchorRef = null; + } + + get platformObject() { + return [ + { + key: this.intl.t('all'), + value: -1, + }, + { + key: this.intl.t('android'), + value: ENUMS.PLATFORM.ANDROID, + }, + { + key: this.intl.t('ios'), + value: ENUMS.PLATFORM.IOS, + }, + ]; + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'Storeknox::Discover::TableColumns::StoreHeader': typeof StoreknoxDiscoverTableColumnsStoreHeaderComponent; + } +} diff --git a/app/components/storeknox/table-columns/store/index.hbs b/app/components/storeknox/table-columns/store/index.hbs new file mode 100644 index 000000000..88dfcd385 --- /dev/null +++ b/app/components/storeknox/table-columns/store/index.hbs @@ -0,0 +1,13 @@ +{{#if @loading}} + + + +{{else}} + {{#if @data.isIos}} + + {{/if}} + + {{#if @data.isAndroid}} + + {{/if}} +{{/if}} \ No newline at end of file diff --git a/app/models/sk-add-to-inventory.ts b/app/models/sk-add-to-inventory.ts new file mode 100644 index 000000000..13585dfa3 --- /dev/null +++ b/app/models/sk-add-to-inventory.ts @@ -0,0 +1,18 @@ +import Model, { attr } from '@ember-data/model'; + +export default class SkAddToInventoryModel extends Model { + @attr('string') + declare docUlid: string; + + @attr('number') + declare appDiscoveryQuery: number; + + @attr('number') + declare approvalStatus: number; +} + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'sk-add-to-inventory': SkAddToInventoryModel; + } +} diff --git a/app/models/sk-app.ts b/app/models/sk-app.ts new file mode 100644 index 000000000..3af4e6531 --- /dev/null +++ b/app/models/sk-app.ts @@ -0,0 +1,189 @@ +import Model, { attr, belongsTo, AsyncBelongsTo } from '@ember-data/model'; + +import ENUMS from 'irene/enums'; +import OrganizationUserModel from './organization-user'; + +export interface AppMetadata { + doc_ulid: string; + doc_hash: string; + app_id: string; + url: string; + icon_url: string; + package_name: string; + title: string; + region: Region; + store: Store; + platform: number; + platform_display: string; + dev_name: string; + dev_email: string; + dev_website: string; + dev_id: string; + rating: number; + rating_count: number; + review_count: number; + total_downloads: number; + upload_date: Date; + latest_upload_date: Date; +} + +export interface Region { + id: number; + sk_store: number; + country_code: string; + icon: string; +} + +export interface Store { + id: number; + name: string; + identifier: string; + icon: string; + platform: number; + platform_display: string; +} + +export interface AvailabilityData { + storeknox: boolean; + appknox: boolean; +} + +enum SkAppStatus { + PENDING_APPROVAL = 0, + APPROVED = 1, + REJECTED = 2, +} + +export default class SkAppModel extends Model { + @attr('number') + declare appDiscoveryQuery: number; + + @attr('number') + declare approvalStatus: number; + + @attr('string') + declare approvalStatusDisplay: string; + + @attr('number') + declare appStatus: number; + + @attr('string') + declare appStatusDisplay: string; + + @attr('number') + declare appSource: number; + + @attr('string') + declare appSourceDisplay: string; + + @attr('boolean') + declare monitoringEnabled: boolean; + + @attr('number') + declare monitoringStatus: number; + + @attr('number') + declare skOrganization: number; + + @attr('date') + declare approvedOn: Date; + + @attr('date') + declare addedOn: Date; + + @attr('date') + declare updatedOn: Date; + + @attr('date') + declare rejectedOn: Date; + + @attr('number', { allowNull: true }) + declare coreProject: number | null; + + @attr() + declare appMetadata: AppMetadata; + + @attr() + declare availability: AvailabilityData; + + @attr() + declare statusButtonsLoading: AvailabilityData; + + @attr('boolean') + declare selected: boolean; + + @belongsTo('organization-user', { async: true, inverse: null }) + declare addedBy: AsyncBelongsTo; + + @belongsTo('organization-user', { async: true, inverse: null }) + declare approvedBy: AsyncBelongsTo; + + @belongsTo('organization-user', { async: true, inverse: null }) + declare rejectedBy: AsyncBelongsTo; + + get docUlid() { + return this.appMetadata.doc_ulid; + } + + get iconUrl() { + return this.appMetadata.icon_url; + } + + get packageName() { + return this.appMetadata.package_name; + } + + get appUrl() { + return this.appMetadata.url; + } + + get title() { + return this.appMetadata.title; + } + + get devName() { + return this.appMetadata.dev_name; + } + + get devEmail() { + return this.appMetadata.dev_email; + } + + get isAndroid() { + return this.appMetadata.platform === ENUMS.PLATFORM.ANDROID; + } + + get isIos() { + return this.appMetadata.platform === ENUMS.PLATFORM.IOS; + } + + get foundBy() { + if (this.appSource === 1) { + return 'Manual Discovery'; + } else { + return 'Auto Discovery'; + } + } + + get status() { + switch (this.approvalStatus) { + case SkAppStatus.PENDING_APPROVAL: + return 'pending'; + + case SkAppStatus.APPROVED: + return 'approved'; + + case SkAppStatus.REJECTED: + return 'rejected'; + + default: + return null; + } + } +} + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'sk-app': SkAppModel; + } +} diff --git a/app/models/sk-discovery-result.ts b/app/models/sk-discovery-result.ts new file mode 100644 index 000000000..5c28c8e46 --- /dev/null +++ b/app/models/sk-discovery-result.ts @@ -0,0 +1,96 @@ +import Model, { attr } from '@ember-data/model'; +import ENUMS from 'irene/enums'; + +export default class SkDiscoverySearchResultModel extends Model { + @attr('string') + declare docUlid: string; + + @attr('string') + declare docHash: string; + + @attr('string') + declare appId: string; + + @attr('string') + declare packageName: string; + + @attr('string') + declare title: string; + + @attr('number') + declare platform: number; + + @attr('string') + declare region: string; + + @attr('number') + declare appSize: number; + + @attr('string') + declare appType: string; + + @attr('string') + declare appUrl: string; + + @attr('boolean') + declare isFree: boolean; + + @attr('string') + declare description: string; + + @attr('string') + declare devName: string; + + @attr('string') + declare iconUrl: string; + + @attr('string') + declare latestUploadDate: string; + + @attr('string') + declare minOsRequired: string; + + @attr('number') + declare rating: number; + + @attr('number') + declare ratingCount: number; + + @attr() + declare screenshots: string[]; + + @attr('string') + declare version: string; + + @attr('string') + declare docCreatedOn: string; + + @attr('string') + declare docUpdatedOn: string; + + @attr('boolean') + declare requested: boolean; + + @attr('boolean') + declare approved: boolean; + + @attr('boolean') + declare addButtonLoading: boolean; + + @attr('boolean') + declare selected: boolean; + + get isAndroid() { + return this.platform === ENUMS.PLATFORM.ANDROID; + } + + get isIos() { + return this.platform === ENUMS.PLATFORM.IOS; + } +} + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'sk-discovery-result': SkDiscoverySearchResultModel; + } +} diff --git a/app/models/sk-discovery.ts b/app/models/sk-discovery.ts new file mode 100644 index 000000000..28a78240c --- /dev/null +++ b/app/models/sk-discovery.ts @@ -0,0 +1,24 @@ +import Model, { attr } from '@ember-data/model'; + +export default class SkDiscoveryModel extends Model { + @attr('number') + declare skOrganization: number; + + @attr('string') + declare queryStr: string; + + @attr('boolean') + declare continuousDiscovery: boolean; + + @attr() + declare query: { q: string }; + + @attr() + declare results: any; +} + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'sk-discovery': SkDiscoveryModel; + } +} diff --git a/app/models/sk-inventory-approval-status.ts b/app/models/sk-inventory-approval-status.ts new file mode 100644 index 000000000..8d3e907ec --- /dev/null +++ b/app/models/sk-inventory-approval-status.ts @@ -0,0 +1,15 @@ +import Model, { attr } from '@ember-data/model'; + +export default class SkInventoryApprovalStatusModel extends Model { + @attr('number') + declare approvalStatus: number; + + @attr('string') + declare approvalStatusDisplay: string; +} + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'sk-inventory-approval-status': SkInventoryApprovalStatusModel; + } +} diff --git a/app/models/sk-requested-app.ts b/app/models/sk-requested-app.ts new file mode 100644 index 000000000..e0448e695 --- /dev/null +++ b/app/models/sk-requested-app.ts @@ -0,0 +1,189 @@ +import Model, { attr, belongsTo, AsyncBelongsTo } from '@ember-data/model'; + +import ENUMS from 'irene/enums'; +import OrganizationUserModel from './organization-user'; + +export interface AppMetadata { + doc_ulid: string; + doc_hash: string; + app_id: string; + url: string; + icon_url: string; + package_name: string; + title: string; + region: Region; + store: Store; + platform: number; + platform_display: string; + dev_name: string; + dev_email: string; + dev_website: string; + dev_id: string; + rating: number; + rating_count: number; + review_count: number; + total_downloads: number; + upload_date: Date; + latest_upload_date: Date; +} + +export interface Region { + id: number; + sk_store: number; + country_code: string; + icon: string; +} + +export interface Store { + id: number; + name: string; + identifier: string; + icon: string; + platform: number; + platform_display: string; +} + +export interface AvailabilityData { + storeknox: boolean; + appknox: boolean; +} + +enum SkAppStatus { + PENDING_APPROVAL = 0, + APPROVED = 1, + REJECTED = 2, +} + +export default class SkRequestedAppModel extends Model { + @attr('number') + declare appDiscoveryQuery: number; + + @attr('number') + declare approvalStatus: number; + + @attr('string') + declare approvalStatusDisplay: string; + + @attr('number') + declare appStatus: number; + + @attr('string') + declare appStatusDisplay: string; + + @attr('number') + declare appSource: number; + + @attr('string') + declare appSourceDisplay: string; + + @attr('boolean') + declare monitoringEnabled: boolean; + + @attr('number') + declare monitoringStatus: number; + + @attr('number') + declare skOrganization: number; + + @attr('date') + declare approvedOn: Date; + + @attr('date') + declare addedOn: Date; + + @attr('date') + declare updatedOn: Date; + + @attr('date') + declare rejectedOn: Date; + + @attr('number', { allowNull: true }) + declare coreProject: number | null; + + @attr() + declare appMetadata: AppMetadata; + + @attr() + declare availability: AvailabilityData; + + @attr() + declare statusButtonsLoading: AvailabilityData; + + @attr('boolean') + declare selected: boolean; + + @belongsTo('organization-user', { async: true, inverse: null }) + declare addedBy: AsyncBelongsTo; + + @attr('string') + declare approvedBy: string; + + @attr('string') + declare rejectedBy: string; + + get docUlid() { + return this.appMetadata.doc_ulid; + } + + get iconUrl() { + return this.appMetadata.icon_url; + } + + get packageName() { + return this.appMetadata.package_name; + } + + get appUrl() { + return this.appMetadata.url; + } + + get title() { + return this.appMetadata.title; + } + + get devName() { + return this.appMetadata.dev_name; + } + + get devEmail() { + return this.appMetadata.dev_email; + } + + get isAndroid() { + return this.appMetadata.platform === ENUMS.PLATFORM.ANDROID; + } + + get isIos() { + return this.appMetadata.platform === ENUMS.PLATFORM.IOS; + } + + get foundBy() { + if (this.appSource === 1) { + return 'Manual Discovery'; + } else { + return 'Auto Discovery'; + } + } + + get status() { + switch (this.approvalStatus) { + case SkAppStatus.PENDING_APPROVAL: + return 'pending'; + + case SkAppStatus.APPROVED: + return 'approved'; + + case SkAppStatus.REJECTED: + return 'rejected'; + + default: + return null; + } + } +} + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'sk-requested-app': SkRequestedAppModel; + } +} diff --git a/app/router.ts b/app/router.ts index 938ecf568..d66469e35 100644 --- a/app/router.ts +++ b/app/router.ts @@ -130,6 +130,16 @@ Router.map(function () { this.route('analytics'); }); + this.route('storeknox', { path: '/dashboard/storeknox' }, function () { + this.route('discover', function () { + this.route('result'); + this.route('requested'); + this.route('review'); + }); + + this.route('review-logs', { path: '/discover/review-logs' }); + }); + this.route('dashboard', function () { this.route('projects'); diff --git a/app/routes/authenticated/storeknox.ts b/app/routes/authenticated/storeknox.ts new file mode 100644 index 000000000..0ab902057 --- /dev/null +++ b/app/routes/authenticated/storeknox.ts @@ -0,0 +1,3 @@ +import Route from '@ember/routing/route'; + +export default class AuthenticatedStoreknoxRoute extends Route {} diff --git a/app/routes/authenticated/storeknox/discover.ts b/app/routes/authenticated/storeknox/discover.ts new file mode 100644 index 000000000..c2689f3b8 --- /dev/null +++ b/app/routes/authenticated/storeknox/discover.ts @@ -0,0 +1,3 @@ +import Route from '@ember/routing/route'; + +export default class AuthenticatedStoreknoxDiscoverRoute extends Route {} diff --git a/app/routes/authenticated/storeknox/discover/requested.ts b/app/routes/authenticated/storeknox/discover/requested.ts new file mode 100644 index 000000000..334ab6740 --- /dev/null +++ b/app/routes/authenticated/storeknox/discover/requested.ts @@ -0,0 +1,25 @@ +import Route from '@ember/routing/route'; + +export interface StoreknoxDiscoveryRequestedQueryParam { + app_limit: string; + app_offset: string; +} + +export default class AuthenticatedStoreknoxDiscoverRequestedRoute extends Route { + queryParams = { + app_limit: { + refreshModel: true, + }, + app_offset: { + refreshModel: true, + }, + }; + + model(params: Partial) { + const { app_limit = '10', app_offset = '0' } = params; + + return { + queryParams: { app_limit, app_offset }, + }; + } +} diff --git a/app/routes/authenticated/storeknox/discover/result.ts b/app/routes/authenticated/storeknox/discover/result.ts new file mode 100644 index 000000000..5db4205f9 --- /dev/null +++ b/app/routes/authenticated/storeknox/discover/result.ts @@ -0,0 +1,38 @@ +import Route from '@ember/routing/route'; + +export interface StoreknoxDiscoveryResultQueryParam { + app_limit: string; + app_offset: string; + app_search_id: string; + app_query: string; +} + +export default class AuthenticatedStoreknoxDiscoverResultRoute extends Route { + queryParams = { + app_limit: { + refreshModel: true, + }, + app_offset: { + refreshModel: true, + }, + app_search_id: { + refreshModel: true, + }, + app_query: { + refreshModel: true, + }, + }; + + model(params: Partial) { + const { + app_limit = '10', + app_offset = '0', + app_search_id = null, + app_query = '', + } = params; + + return { + queryParams: { app_limit, app_offset, app_search_id, app_query }, + }; + } +} diff --git a/app/routes/authenticated/storeknox/discover/review.ts b/app/routes/authenticated/storeknox/discover/review.ts new file mode 100644 index 000000000..588f176f5 --- /dev/null +++ b/app/routes/authenticated/storeknox/discover/review.ts @@ -0,0 +1,25 @@ +import Route from '@ember/routing/route'; + +export interface StoreknoxDiscoveryReviewQueryParam { + app_limit: string; + app_offset: string; +} + +export default class AuthenticatedStoreknoxDiscoverReviewRoute extends Route { + queryParams = { + app_limit: { + refreshModel: true, + }, + app_offset: { + refreshModel: true, + }, + }; + + model(params: Partial) { + const { app_limit = '10', app_offset = '0' } = params; + + return { + queryParams: { app_limit, app_offset }, + }; + } +} diff --git a/app/routes/authenticated/storeknox/review-logs.ts b/app/routes/authenticated/storeknox/review-logs.ts new file mode 100644 index 000000000..011da1378 --- /dev/null +++ b/app/routes/authenticated/storeknox/review-logs.ts @@ -0,0 +1,3 @@ +import Route from '@ember/routing/route'; + +export default class AuthenticatedStoreknoxReviewLogsRoute extends Route {} diff --git a/app/services/sk-discovery-result.ts b/app/services/sk-discovery-result.ts new file mode 100644 index 000000000..00d00a53b --- /dev/null +++ b/app/services/sk-discovery-result.ts @@ -0,0 +1,279 @@ +import Service, { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import { task } from 'ember-concurrency'; +import Store from '@ember-data/store'; +// eslint-disable-next-line ember/use-ember-data-rfc-395-imports +import { DS } from 'ember-data'; +import { waitForPromise } from '@ember/test-waiters'; +import RouterService from '@ember/routing/router-service'; + +import SkDiscoverySearchResultModel from 'irene/models/sk-discovery-result'; + +type SkDiscoveryResultResponse = + DS.AdapterPopulatedRecordArray & { + meta: { count: number }; + }; + +type skDiscoveryResultObjType = { + [id: string]: number; +}; + +export const DEFAULT_PROJECT_QUERY_PARAMS = { + query: '', + platform: -1, +}; + +export default class SkDiscoveryResultService extends Service { + @service declare store: Store; + @service('notifications') declare notify: NotificationService; + @service declare router: RouterService; + @service('browser/window') declare window: Window; + + @tracked skDiscoveryResultData: SkDiscoveryResultResponse | null = null; + @tracked skDiscoveryResultObj: skDiscoveryResultObjType = {}; + @tracked searchId: string | number = ''; + @tracked selectedResults: string[] = []; + @tracked allRowsSelected: boolean = false; + @tracked requestedCount = 0; + + get isFetchingData() { + return ( + this.fetchDiscoveryResults.isRunning || this.uploadSearchQuery.isRunning + ); + } + + uploadSearchQuery = task( + async ( + searchQuery: string, + limit: string | number, + offset: string | number, + setQueryParams = false + ) => { + try { + const discoveryQuery = this.store.createRecord('skDiscovery', { + queryStr: searchQuery, + }); + + const uploadedDiscovery = await waitForPromise(discoveryQuery.save()); + + this.router.transitionTo('authenticated.storeknox.discover.result', { + queryParams: { app_search_id: uploadedDiscovery.id }, + }); + + this.fetchDiscoveryResults.perform( + limit, + offset, + uploadedDiscovery.id, + setQueryParams + ); + } catch (error) { + console.log(error); + } + } + ); + + reset() { + this.selectedResults = []; + this.skDiscoveryResultObj = {}; + this.allRowsSelected = false; + this.requestedCount = 0; + } + + fetchDiscoveryResults = task( + { drop: true }, + async ( + limit: string | number, + offset: string | number, + searchId: string | number, + setQueryParams = true + ) => { + this.reset(); + + this.searchId = searchId; + + if (setQueryParams) { + this.setRouteQueryParams(limit, offset, searchId); + } + + try { + this.skDiscoveryResultData = (await this.store.query( + 'skDiscoveryResult', + { + limit, + offset, + id: searchId, + } + )) as SkDiscoveryResultResponse; + + this.removeQueryParamFromStoredUrl(); + + for (const [index, record] of (this.skDiscoveryResultData + ? this.skDiscoveryResultData.slice() + : [] + ).entries()) { + this.skDiscoveryResultObj[record.docUlid] = index; + + try { + const approvalStatus = await this.store.queryRecord( + 'skInventoryApprovalStatus', + { + doc_ulid: record.docUlid, + } + ); + + record.set('requested', true); + + if (approvalStatus.approvalStatus === 1) { + record.set('approved', true); + } + + this.requestedCount++; + } catch (e: any) { + if (e?.errors && e.errors[0]?.status === '404') { + record.set('requested', false); + + record.set('selected', false); + } + } + } + } catch (e) { + console.log(e); + } + } + ); + + removeQueryParamFromStoredUrl() { + const transInfo = this.window.sessionStorage.getItem('_lastTransitionInfo'); + + if (transInfo) { + const transInfoObj = JSON.parse(transInfo); + + if (transInfoObj.url) { + transInfoObj.url = transInfoObj.url.split('?')[0]; + } + + this.window.sessionStorage.setItem( + '_lastTransitionInfo', + JSON.stringify(transInfoObj) + ); + } + } + + addToInventory = task(async (ulid: string, multiple?: boolean) => { + const index = this.skDiscoveryResultObj[ulid] as number; + + const selectedResult = this.skDiscoveryResultData + ? this.skDiscoveryResultData.slice()[index] + : null; + + selectedResult?.set('addButtonLoading', true); + + try { + const addToInventoryQuery = this.store.createRecord('skAddToInventory', { + docUlid: ulid, + appDiscoveryQuery: this.searchId, + }); + + const res = await waitForPromise(addToInventoryQuery.save()); + + selectedResult?.set('requested', true); + + if (res.approvalStatus === 1) { + selectedResult?.set('approved', true); + + this.notify.success('Added to App Inventory'); + } else if (res.approvalStatus === 0) { + this.notify.success('Request has been successfully sent'); + } + + this.requestedCount++; + + if (this.selectedResults.includes(ulid)) { + this.selectRow(ulid, false); + } + + if (multiple && this.selectedResults.length === 0) { + this.allRowsSelected = false; + } + } catch (error) { + console.log(error); + } + + selectedResult?.set('addButtonLoading', false); + + if (multiple && this.selectedResults.length === 0) { + this.notify.success('All requests have been successfully sent'); + } + }); + + addMultipleToInventory = task(async () => { + this.selectedResults.forEach((ulid) => { + this.addToInventory.perform(ulid, true); + }); + }); + + setRouteQueryParams( + limit: string | number, + offset: string | number, + searchId: string | number + ) { + this.router.transitionTo({ + queryParams: { + app_limit: limit, + app_offset: offset, + app_search_id: searchId, + }, + }); + } + + selectRow(ulid: string, value: boolean, force?: boolean) { + const index = this.skDiscoveryResultObj[ulid] as number; + + const selectedResult = this.skDiscoveryResultData + ? this.skDiscoveryResultData.slice()[index] + : null; + + selectedResult?.set('selected', value); + + if (force && !value) { + return; + } + + const idx = this.selectedResults.indexOf(ulid); + + if (idx !== -1) { + this.selectedResults.removeObject(ulid); + } else { + this.selectedResults.pushObject(ulid); + } + + this.checkIfAllSelected(); + } + + selectAllRow(value: boolean) { + this.selectedResults.clear(); + + for (const record of this.skDiscoveryResultData + ? this.skDiscoveryResultData.slice() + : []) { + if (!record.requested) { + this.selectRow(record.docUlid, value, true); + } + } + } + + checkIfAllSelected() { + const discoveryResultDataLength = this.skDiscoveryResultData + ? this.skDiscoveryResultData.slice().length + : 0; + + if ( + this.requestedCount + this.selectedResults.length === + discoveryResultDataLength + ) { + this.allRowsSelected = true; + } else { + this.allRowsSelected = false; + } + } +} diff --git a/app/services/sk-pending-review.ts b/app/services/sk-pending-review.ts new file mode 100644 index 000000000..526618dea --- /dev/null +++ b/app/services/sk-pending-review.ts @@ -0,0 +1,216 @@ +import Service, { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import { task } from 'ember-concurrency'; +import Store from '@ember-data/store'; +// eslint-disable-next-line ember/use-ember-data-rfc-395-imports +import { DS } from 'ember-data'; +import { waitForPromise } from '@ember/test-waiters'; +import RouterService from '@ember/routing/router-service'; +import { action } from '@ember/object'; + +import SkAppModel from 'irene/models/sk-app'; + +type SkPendingReviewResponse = DS.AdapterPopulatedRecordArray & { + meta: { count: number }; +}; + +type skPendingReviewObjType = { + [id: string]: number; +}; + +export const DEFAULT_PROJECT_QUERY_PARAMS = { + query: '', + platform: -1, +}; + +export default class SkPendingReviewService extends Service { + @service declare store: Store; + @service('notifications') declare notify: NotificationService; + @service declare router: RouterService; + + @tracked skPendingReviewData: SkPendingReviewResponse | null = null; + @tracked skPendingReviewObj: skPendingReviewObjType = {}; + @tracked selectedApps: string[] = []; + @tracked allRowsSelected: boolean = false; + @tracked limit: string | number = 10; + @tracked offset: string | number = 0; + @tracked showLoading: boolean = true; + + get isFetchingData() { + return this.fetchPendingReviewApps.isRunning && this.showLoading; + } + + reset() { + this.selectedApps = []; + this.skPendingReviewObj = {}; + this.allRowsSelected = false; + } + + fetchPendingReviewApps = task( + { drop: true }, + async ( + limit: string | number = 10, + offset: string | number = 0, + setQueryParams = true + ) => { + this.limit = limit; + this.offset = offset; + + if (!this.showLoading) { + this.reset(); + } + + if (setQueryParams) { + this.setRouteQueryParams(limit, offset); + } + + try { + this.skPendingReviewData = (await this.store.query('skApp', { + limit, + offset, + approval_status: 0, + app_status: 1, + })) as SkPendingReviewResponse; + + for (const [index, record] of (this.skPendingReviewData + ? this.skPendingReviewData.slice() + : [] + ).entries()) { + this.skPendingReviewObj[record.id] = index; + } + + this.showLoading = true; + } catch (e) { + console.log(e); + } + } + ); + + approveRejectApp = task( + async (id: string, approve: boolean, multiple?: boolean) => { + const index = this.skPendingReviewObj[id] as number; + + const selectedAppValue = this.skPendingReviewData + ? this.skPendingReviewData.slice()[index] + : null; + + selectedAppValue?.set('statusButtonsLoading', true); + + try { + if (approve) { + await waitForPromise(this.approveApp(id)); + } else { + await waitForPromise(this.rejectApp(id)); + } + + if (this.selectedApps.includes(id)) { + this.selectRow(id, false); + } + + if (multiple && this.selectedApps.length === 0) { + this.allRowsSelected = false; + } + + if ( + Number(this.offset) > 0 && + this.totalCount - Number(this.offset) === 1 + ) { + this.offset = Number(this.offset) - Number(this.limit); + } + + this.fetchPendingReviewApps.perform(this.limit, this.offset); + + this.showLoading = false; + + if (approve) { + this.notify.success( + `${selectedAppValue?.title} App has been moved to App Inventory` + ); + } else { + this.notify.success( + `${selectedAppValue?.title} App has been rejected` + ); + } + } catch (error) { + console.log(error); + } + + selectedAppValue?.set('statusButtonsLoading', false); + } + ); + + approveRejectMultipleApp = task(async (approve: boolean) => { + this.selectedApps.forEach((id) => { + this.approveRejectApp.perform(id, approve, true); + }); + }); + + setRouteQueryParams(limit: string | number, offset: string | number) { + this.router.transitionTo({ + queryParams: { + app_limit: limit, + app_offset: offset, + }, + }); + } + + selectRow(id: string, value: boolean, force?: boolean) { + const index = this.skPendingReviewObj[id] as number; + + const selectedResult = this.skPendingReviewData + ? this.skPendingReviewData.slice()[index] + : null; + + selectedResult?.set('selected', value); + + if (force && !value) { + return; + } + + const idx = this.selectedApps.indexOf(id); + + if (idx !== -1) { + this.selectedApps.removeObject(id); + } else { + this.selectedApps.pushObject(id); + } + + this.checkIfAllSelected(); + } + + @action + async approveApp(id: string) { + const adapter = this.store.adapterFor('sk-app'); + + return await adapter.approveApp(id); + } + + @action + async rejectApp(id: string) { + const adapter = this.store.adapterFor('sk-app'); + + return await adapter.rejectApp(id); + } + + selectAllRow(value: boolean) { + this.selectedApps.clear(); + + for (const record of this.skPendingReviewData + ? this.skPendingReviewData.slice() + : []) { + this.selectRow(record.id, value, true); + } + } + + checkIfAllSelected() { + const pendingReviewDataLength = this.skPendingReviewData + ? this.skPendingReviewData.slice().length + : 0; + + this.allRowsSelected = this.selectedApps.length === pendingReviewDataLength; + } + + get totalCount() { + return this.skPendingReviewData?.meta?.count || 0; + } +} diff --git a/app/services/sk-requested-app.ts b/app/services/sk-requested-app.ts new file mode 100644 index 000000000..ff6b11e28 --- /dev/null +++ b/app/services/sk-requested-app.ts @@ -0,0 +1,66 @@ +import Service, { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import { task } from 'ember-concurrency'; +import Store from '@ember-data/store'; +// eslint-disable-next-line ember/use-ember-data-rfc-395-imports +import { DS } from 'ember-data'; +import RouterService from '@ember/routing/router-service'; + +import SkRequestedAppModel from 'irene/models/sk-requested-app'; + +type SkRequestedAppResponse = + DS.AdapterPopulatedRecordArray & { + meta: { count: number }; + }; + +export const DEFAULT_PROJECT_QUERY_PARAMS = { + query: '', + platform: -1, +}; + +export default class SkRequestedAppService extends Service { + @service declare store: Store; + @service('notifications') declare notify: NotificationService; + @service declare router: RouterService; + + @tracked skRequestedAppData: SkRequestedAppResponse | null = null; + + get isFetchingData() { + return this.fetchRequestedApps.isRunning; + } + + fetchRequestedApps = task( + { drop: true }, + async ( + limit: string | number, + offset: string | number, + setQueryParams = true + ) => { + if (setQueryParams) { + this.setRouteQueryParams(limit, offset); + } + + try { + this.skRequestedAppData = (await this.store.query('skRequestedApp', { + limit, + offset, + })) as SkRequestedAppResponse; + } catch (e) { + console.log(e); + } + } + ); + + setRouteQueryParams(limit: string | number, offset: string | number) { + this.router.transitionTo({ + queryParams: { + app_limit: limit, + app_offset: offset, + }, + }); + } + + get totalCount() { + return this.skRequestedAppData?.meta?.count || 0; + } +} diff --git a/app/styles/_component-variables.scss b/app/styles/_component-variables.scss index d44b99adb..b0bbf35d1 100644 --- a/app/styles/_component-variables.scss +++ b/app/styles/_component-variables.scss @@ -1522,6 +1522,127 @@ body { // variables for marketplace/integration-card --marketplace-integration-card-text-color: var(--text-secondary); --marketplace-integration-card-border-color: var(--border-color-1); + + // variables for storeknox/discover/pending-review/table/availability-header + --storeknox-discover-pending-review-table-availability-header-info-icon-color: var( + --neutral-grey-400 + ); + --storeknox-discover-pending-review-table-availability-filter-background-color: var( + --background-main + ); + --storeknox-discover-pending-review-table-availability-filter-box-shadow: var( + --box-shadow-7 + ); + --storeknox-discover-pending-review-table-availability-filter-option-hover-background-color: var( + --neutral-grey-200 + ); + --storeknox-discover-pending-review-table-availability-filter-option-clear-filter-background-color: var( + --neutral-grey-200 + ); + + // variables for storeknox/discover/pending-review/table/found-by + --storeknox-discover-pending-review-table-found-by-info-icon-color: var( + --neutral-grey-300 + ); + + // variables for storeknox/discover/pending-review/table/found-by-header + --storeknox-discover-pending-review-table-found-by-filter-background-color: var( + --background-main + ); + --storeknox-discover-pending-review-table-found-by-filter-box-shadow: var( + --box-shadow-7 + ); + --storeknox-discover-pending-review-table-found-by-filter-option-hover-background-color: var( + --neutral-grey-200 + ); + --storeknox-discover-pending-review-table-found-by-filter-option-clear-filter-background-color: var( + --neutral-grey-200 + ); + + // variables for storeknox/discover/pending-review/table/status + --storeknox-discover-pending-review-table-status-info-icon-color: var( + --neutral-grey-300 + ); + + // variables for storeknox/discover/pending-review + --storeknox-discover-pending-review-header-border-color: var( + --neutral-grey-200 + ); + --storeknox-discover-pending-review-header-bg-color: var(--common-white); + --storeknox-discover-pending-review-approve-button-color: var(--success-main); + --storeknox-discover-pending-review-reject-button-color: var(--error-main); + --storeknox-discover-pending-review-header-divider-background-color: var( + --neutral-grey-200 + ); + + // variables for storeknox/discover/requested-apps/table + --storeknox-discover-requested-apps-table-row-color: var(--common-white); + --storeknox-discover-requested-apps-table-row-border-color: var( + --neutral-grey-100 + ); + + // variables for storeknox/discover/requested-apps/table/status + --storeknox-discover-requested-apps-table-status-info-icon-color: var( + --neutral-grey-300 + ); + + // variables for storeknox/discover/results/table + --storeknox-discover-results-table-row-color: var(--common-white); + --storeknox-discover-results-table-row-border-color: var(--neutral-grey-100); + + // variables for storeknox/discover/results/table/action + --storeknox-discover-results-table-action-requested-icon-color: var( + --warn-dark + ); + --storeknox-discover-results-table-action-requested-icon-bgcolor: var( + --warn-light + ); + --storeknox-discover-results-table-action-already-exist-icon-color: var( + --success-main + ); + --storeknox-discover-results-table-action-already-exist-icon-bgcolor: var( + --success-light + ); + + // variables for storeknox/discover/results + --storeknox-discover-results-input-bg-color: var(--common-white); + + // variables for storeknox/discover/results/table + --storeknox-discover-results-table-header-border-color: var( + --neutral-grey-200 + ); + --storeknox-discover-results-table-header-bg-color: var(--common-white); + + // variables for storeknox/discover + --storeknox-discover-header-border-color: var(--neutral-grey-200); + --storeknox-discover-header-background-color: var(--common-white); + --storeknox-discover-header-description-color: var(--neutral-grey-500); + + // variables for storeknox/discover/pending-review/table + --storeknox-discover-pending-review-table-row-color: var(--common-white); + --storeknox-discover-pending-review-table-row-border-color: var( + --neutral-grey-100 + ); + + // variables for storeknox/review-logs + --storeknox-review-logs-header-background-color: var(--common-white); + --storeknox-review-logs-header-border-color: var(--neutral-grey-200); + --storeknox-review-logs-header-description-color: var(--neutral-grey-500); + + // variables for storeknox/table-columns/store-header + --storeknox-table-columns-store-header-filter-background-color: var( + --background-main + ); + --storeknox-table-columns-store-header-filter-box-shadow: var(--box-shadow-7); + --storeknox-table-columns-store-header-filter-option-hover-bgcolor: var( + --neutral-grey-200 + ); + --storeknox-table-columns-store-header-filter-option-clear-filter-bgcolor: var( + --neutral-grey-200 + ); + + // variables for page-wrapper + --page-wrapper-background-color: var(--background-light); } body { diff --git a/app/styles/_icons.scss b/app/styles/_icons.scss index 367be42f2..b242f9e14 100644 --- a/app/styles/_icons.scss +++ b/app/styles/_icons.scss @@ -578,3 +578,11 @@ .ak-icon-stop-circle { @extend .mi-stop-circle; } + +.ak-icon-schedule-send { + @extend .mi-schedule-send; +} + +.ak-icon-add-box { + @extend .mi-add-box; +} diff --git a/app/templates/authenticated/storeknox.hbs b/app/templates/authenticated/storeknox.hbs new file mode 100644 index 000000000..74a477e81 --- /dev/null +++ b/app/templates/authenticated/storeknox.hbs @@ -0,0 +1,3 @@ +{{page-title 'Storeknox'}} + +{{outlet}} \ No newline at end of file diff --git a/app/templates/authenticated/storeknox/discover.hbs b/app/templates/authenticated/storeknox/discover.hbs new file mode 100644 index 000000000..f14f79ad9 --- /dev/null +++ b/app/templates/authenticated/storeknox/discover.hbs @@ -0,0 +1,7 @@ +{{page-title 'Discover'}} + + + + + {{outlet}} + \ No newline at end of file diff --git a/app/templates/authenticated/storeknox/discover/requested.hbs b/app/templates/authenticated/storeknox/discover/requested.hbs new file mode 100644 index 000000000..348be196a --- /dev/null +++ b/app/templates/authenticated/storeknox/discover/requested.hbs @@ -0,0 +1,3 @@ +{{page-title 'Requested'}} + + \ No newline at end of file diff --git a/app/templates/authenticated/storeknox/discover/result.hbs b/app/templates/authenticated/storeknox/discover/result.hbs new file mode 100644 index 000000000..664f64967 --- /dev/null +++ b/app/templates/authenticated/storeknox/discover/result.hbs @@ -0,0 +1,3 @@ +{{page-title 'Result'}} + + \ No newline at end of file diff --git a/app/templates/authenticated/storeknox/discover/review.hbs b/app/templates/authenticated/storeknox/discover/review.hbs new file mode 100644 index 000000000..ac067ffd1 --- /dev/null +++ b/app/templates/authenticated/storeknox/discover/review.hbs @@ -0,0 +1,3 @@ +{{page-title 'Review'}} + + \ No newline at end of file diff --git a/app/templates/authenticated/storeknox/review-logs.hbs b/app/templates/authenticated/storeknox/review-logs.hbs new file mode 100644 index 000000000..91b151fc9 --- /dev/null +++ b/app/templates/authenticated/storeknox/review-logs.hbs @@ -0,0 +1,5 @@ +{{page-title 'Review Logs'}} + + + + \ No newline at end of file diff --git a/translations/en.json b/translations/en.json index 6795c54e4..fc7a68497 100644 --- a/translations/en.json +++ b/translations/en.json @@ -660,6 +660,7 @@ "invoice": "INVOICE", "invoiceId": "INVOICE ID", "invoices": "Invoices", + "ios": "iOS", "issueDetails": "Issue Details", "issuedOn": "Issued On", "issuer": "Issuer", @@ -1493,6 +1494,47 @@ "stop": "Stop", "storage": "Storage", "storageManagement": "Storage Management", + "storeknox": { + "actionHeaderInfo": "Your request to add an app will be sent to all Owners in your Organization. If approved, you will be able to view the app in your Organizations Inventory.", + "actionHeaderInfoOwner": "Cick on the ‘+’ icon to add the app to your Organization’s Inventolry.", + "addToInventory": "Add to inventory", + "appAlreadyExists": "This app already exists in your Organization's Inventory", + "appAlreadyRequested": "This app was already requested to be added to your Inventory. Please follow up with an Appknox Owner in your organization for reviewing the request.", + "approved": "Approved", + "autoDiscovery": "Auto Discovery", + "availability": "Availability", + "availableColumnInfo": "This column indicates whether the app is present only on Appknox or Storeknox or on both products", + "discoverHeader": "Discover", + "discoverDescription": "Search for your apps on Google Play Store and Apple App Store and add them to your Organizations Inventory for automated monitoring.", + "discoveryResults": "Discovery Results", + "errorSearchCharacter": "Minimum 2 characters required", + "foundBy": "Found By", + "homeTitle": "Home", + "info": "INFO", + "infoIndicatorWhitelabelText": "This app is not available on VAPT and Store Monitoring", + "logs": "Logs", + "manualDiscovery": "Manual Discovery", + "noPendingItems": "No Pending Items", + "noPendingItemsDescription": "You have reviewed all the requests that was raised by your users.", + "noResultFound": "No Result Found", + "noResultFoundDescription": "We couldn’t find any results for your search keyword. Try refining your search by checking for typos, using different keywords, or being more specific.", + "noRequestedAppsFound": "No Requested Apps Found", + "noRequestedAppsFoundDescription": "You have not requested any apps to be added to your organization's inventory.
Use the Discovery page to search for apps and raise a request.", + "pendingReview": "Pending Review", + "reviewLogs": "Review Logs", + "requestedApps": "Requested Apps", + "requestedBy": "Requested By", + "searchForApps": "Search for Apps", + "searchForAppsDescription": "Use the search bar on this page to look for apps that belong to your Organization and add them to your Inventory for automated monitoring", + "searchQuery": "Search by app name, developer name, namespace or support email", + "sendRequest": "Send Request", + "showingResults": "Showing Results for", + "smIndicatorText": "This App is part of Store Monitoring", + "store": "Store", + "vapt": "VAPT", + "vaptIndicatorText": "This App is part of VAPT", + "waitingForApproval": "Waiting for approval" + }, "storeLowercase": "store", "submit": "Submit", "submitUrl": "Submit URL", diff --git a/translations/ja.json b/translations/ja.json index 3c9e8b5d5..bedc2893e 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -660,6 +660,7 @@ "invoice": "請求書", "invoiceId": "請求書ID", "invoices": "請求書", + "ios": "iOS", "issueDetails": "Issue Details", "issuedOn": "Issued On", "issuer": "Issuer", @@ -1493,6 +1494,47 @@ "stop": "停止中", "storage": "Storage", "storageManagement": "Storage Management", + "storeknox": { + "actionHeaderInfo": "Your request to add an app will be sent to all Owners in your Organization. If approved, you will be able to view the app in your Organizations Inventory.", + "actionHeaderInfoOwner": "Cick on the ‘+’ icon to add the app to your Organization’s Inventolry.", + "addToInventory": "Add to inventory", + "appAlreadyExists": "This app already exists in your Organization's Inventory", + "appAlreadyRequested": "This app was already requested to be added to your Inventory. Please follow up with an Appknox Owner in your organization for reviewing the request.", + "approved": "Approved", + "autoDiscovery": "Auto Discovery", + "availability": "Availability", + "availableColumnInfo": "This column indicates whether the app is present only on Appknox or Storeknox or on both products", + "discoverHeader": "Discover", + "discoverDescription": "Search for your apps on Google Play Store and Apple App Store and add them to your Organizations Inventory for automated monitoring.", + "discoveryResults": "Discovery Results", + "errorSearchCharacter": "Minimum 2 characters required", + "foundBy": "Found By", + "homeTitle": "Home", + "info": "INFO", + "infoIndicatorWhitelabelText": "This app is not available on VAPT and Store Monitoring", + "logs": "Logs", + "manualDiscovery": "Manual Discovery", + "noPendingItems": "No Pending Items", + "noPendingItemsDescription": "You have reviewed all the requests that was raised by your users.", + "noResultFound": "No Result Found", + "noResultFoundDescription": "We couldn’t find any results for your search keyword. Try refining your search by checking for typos, using different keywords, or being more specific.", + "noRequestedAppsFound": "No Requested Apps Found", + "noRequestedAppsFoundDescription": "You have not requested any apps to be added to your organization's inventory.
Use the Discovery page to search for apps and raise a request.", + "pendingReview": "Pending Review", + "reviewLogs": "Review Logs", + "requestedApps": "Requested Apps", + "requestedBy": "Requested By", + "searchForApps": "Search for Apps", + "searchForAppsDescription": "Use the search bar on this page to look for apps that belong to your Organization and add them to your Inventory for automated monitoring", + "searchQuery": "Search by app name, developer name, namespace or support email", + "sendRequest": "Send Request", + "showingResults": "Showing Results for", + "smIndicatorText": "This App is part of Store Monitoring", + "store": "Store", + "vapt": "VAPT", + "vaptIndicatorText": "This App is part of VAPT", + "waitingForApproval": "Waiting for approval" + }, "storeLowercase": "store", "submit": "Submit", "submitUrl": "URLを送信", diff --git a/types/ak-svg.d.ts b/types/ak-svg.d.ts index a3f016f6a..05568c88c 100644 --- a/types/ak-svg.d.ts +++ b/types/ak-svg.d.ts @@ -42,6 +42,13 @@ export enum AkSvgComponentInvocationByNames { DastAutomationUpselling, NoApiUrlFilter, ToggleAutomatedDast, + StoreknoxSearchApps, + StoreknoxPlaystoreLogo, + SmIndicator, + VaptIndicator, + InfoIndicator, + NoPendingItems, + WelcomeToStoreknox, } export enum AkSvgComponentInvocationByPaths {