From a0fe50fd66f9d5ee9c475291ecda64fb4e6d65dd Mon Sep 17 00:00:00 2001 From: futa-ikeda <51409893+futa-ikeda@users.noreply.github.com> Date: Wed, 11 Dec 2024 13:00:00 -0500 Subject: [PATCH 1/2] [ENG-5024] Feature/dashboard b and i (#2429) - Ticket: [ENG-5024] - Feature flag: `share_download` for downloading projects, registrations, and preprint data ## Purpose - Add download functionality to Institutional Dashboard - Add date of report to Summary and Users tab of Institutional Dashboard - Update language and styling --- app/config/environment.d.ts | 1 + .../chart-kpi-wrapper/component-test.ts | 8 +-- .../chart-kpi-wrapper/component.ts | 6 +- .../institutional-users-list/component.ts | 28 +++++++- .../institutional-users-list/styles.scss | 19 +++++- .../institutional-users-list/template.hbs | 39 ++++++++--- .../-components/object-list/component-test.ts | 11 ++++ .../-components/object-list/component.ts | 34 ++++++++++ .../-components/object-list/styles.scss | 12 ++++ .../-components/object-list/template.hbs | 64 ++++++++++++------- app/institutions/dashboard/index/styles.scss | 6 ++ app/institutions/dashboard/index/template.hbs | 10 ++- app/institutions/dashboard/route.ts | 5 +- app/institutions/dashboard/users/styles.scss | 5 ++ app/institutions/dashboard/users/template.hbs | 8 +++ app/models/index-card-search.ts | 2 + app/models/institution-summary-metric.ts | 2 +- app/models/institution-user.ts | 1 + app/serializers/index-card-search.ts | 14 ++++ config/environment.js | 1 + .../index-card-searcher/component.ts | 3 + .../index-card-searcher/template.hbs | 1 + .../factories/institution-summary-metric.ts | 3 + mirage/factories/institution-user.ts | 3 + mirage/factories/root.ts | 2 + mirage/views/search.ts | 6 +- .../acceptance/institutions/dashboard-test.ts | 2 + .../component-test.ts | 22 ++++--- translations/en-us.yml | 12 ++-- 29 files changed, 267 insertions(+), 63 deletions(-) diff --git a/app/config/environment.d.ts b/app/config/environment.d.ts index f564ca7a2b3..2f955b0246c 100644 --- a/app/config/environment.d.ts +++ b/app/config/environment.d.ts @@ -196,6 +196,7 @@ declare const config: { registrationFilesPage: string; verifyEmailModals: string; egapAdmins: string; + shareDownload: string; }; gReCaptcha: { siteKey: string; diff --git a/app/institutions/dashboard/-components/chart-kpi-wrapper/component-test.ts b/app/institutions/dashboard/-components/chart-kpi-wrapper/component-test.ts index 268b43ef98e..c72272b1d6a 100644 --- a/app/institutions/dashboard/-components/chart-kpi-wrapper/component-test.ts +++ b/app/institutions/dashboard/-components/chart-kpi-wrapper/component-test.ts @@ -113,7 +113,7 @@ module('Integration | institutions | dashboard | -components | kpi-chart-wrapper .doesNotExist(); }); - test('it calculates the Public vs Private Registration data correctly', async function(assert) { + test('it calculates the Public vs Embargoed Registration data correctly', async function(assert) { // Given the component is rendered await render(hbs` { + userURL.searchParams.set(key, value); + }); + return userURL.toString(); + } + + get downloadCsvUrl() { + return this.downloadUrl('csv'); + } + + get downloadTsvUrl() { + return this.downloadUrl('tsv'); + } + + get downloadJsonUrl() { + return this.downloadUrl('json_report'); + } + @restartableTask @waitFor async searchDepartment(name: string) { @@ -237,5 +262,4 @@ export default class InstitutionalUsersList extends Component - - - - + + + {{t 'institutions.dashboard.format_labels.json'}} + diff --git a/app/institutions/dashboard/-components/object-list/component-test.ts b/app/institutions/dashboard/-components/object-list/component-test.ts index 4c1ae753cb3..16fda865260 100644 --- a/app/institutions/dashboard/-components/object-list/component-test.ts +++ b/app/institutions/dashboard/-components/object-list/component-test.ts @@ -5,6 +5,10 @@ import { setupIntl } from 'ember-intl/test-support'; import { setupRenderingTest } from 'ember-qunit'; import { TestContext } from 'ember-test-helpers'; import { module, test } from 'qunit'; +import Service from '@ember/service'; +const featuresStub = Service.extend({ + isEnabled: () => true, +}); import { OsfLinkRouterStub } from 'ember-osf-web/tests/integration/helpers/osf-link-router-stub'; @@ -14,6 +18,7 @@ module('Integration | institutions | dashboard | -components | object-list', hoo setupIntl(hooks); hooks.beforeEach(function(this: TestContext) { + this.owner.register('service:features', featuresStub); this.owner.unregister('service:router'); this.owner.register('service:router', OsfLinkRouterStub); const columns = [ @@ -81,6 +86,12 @@ module('Integration | institutions | dashboard | -components | object-list', hoo // The table data is not blatantly incorrect assert.dom('[data-test-object-table-body-row]').exists({ count: 10 }, 'There are 10 rows'); + + // Download buttons are present + await click('[data-test-download-dropdown]'); + assert.dom('[data-test-download-csv-link]').exists('CSV download link exists'); + assert.dom('[data-test-download-tsv-link]').exists('TSV download link exists'); + assert.dom('[data-test-download-json-link]').exists('JSON download link exists'); }); test('the table supports filtering', async function(assert) { diff --git a/app/institutions/dashboard/-components/object-list/component.ts b/app/institutions/dashboard/-components/object-list/component.ts index 0333bfb59ad..53dd0f146d3 100644 --- a/app/institutions/dashboard/-components/object-list/component.ts +++ b/app/institutions/dashboard/-components/object-list/component.ts @@ -1,11 +1,17 @@ import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; +import Features from 'ember-feature-flags/services/features'; +import IndexCardSearchModel from 'ember-osf-web/models/index-card-search'; import InstitutionModel from 'ember-osf-web/models/institution'; import { SuggestedFilterOperators } from 'ember-osf-web/models/related-property-path'; import SearchResultModel from 'ember-osf-web/models/search-result'; import { Filter } from 'osf-components/components/search-page/component'; +import config from 'ember-osf-web/config/environment'; + +const shareDownloadFlag = config.featureFlagNames.shareDownload; interface Column { name: string; @@ -36,6 +42,7 @@ interface InstitutionalObjectListArgs { } export default class InstitutionalObjectList extends Component { + @service features!: Features; @tracked activeFilters: Filter[] = []; @tracked page = ''; @tracked sort = '-dateModified'; @@ -73,6 +80,33 @@ export default class InstitutionalObjectList extends Component {{/if}} - - - - - - - - - + + + + + {{t 'institutions.dashboard.format_labels.csv'}} + + + {{t 'institutions.dashboard.format_labels.tsv'}} + + + {{t 'institutions.dashboard.format_labels.json'}} + + + + {{/if}} diff --git a/app/institutions/dashboard/index/styles.scss b/app/institutions/dashboard/index/styles.scss index 5df5cd3eeaf..ac54ba7ddbb 100644 --- a/app/institutions/dashboard/index/styles.scss +++ b/app/institutions/dashboard/index/styles.scss @@ -22,3 +22,9 @@ } } } + +.updated-at { + align-self: flex-end; + margin-right: 12px; + margin-bottom: 12px; +} diff --git a/app/institutions/dashboard/index/template.hbs b/app/institutions/dashboard/index/template.hbs index 36145181db9..64920d880e8 100644 --- a/app/institutions/dashboard/index/template.hbs +++ b/app/institutions/dashboard/index/template.hbs @@ -1,7 +1,15 @@ - + {{#if this.model.summaryMetrics.reportYearmonth}} +
+ {{t 'institutions.dashboard.updated' date=this.model.summaryMetrics.reportYearmonth}} +
+ {{/if}}
= await institution.queryHasMany( 'userMetrics', - { size: 0 }, + { size: 1 }, ); + const userMetric = userMetricInfo.toArray()[0]; return { institution, departmentMetrics, summaryMetrics, - totalUsers: userMetricInfo.meta.total, + userMetric, }; } catch (error) { captureException(error); diff --git a/app/institutions/dashboard/users/styles.scss b/app/institutions/dashboard/users/styles.scss index 93ca0026725..6e1e46081cd 100644 --- a/app/institutions/dashboard/users/styles.scss +++ b/app/institutions/dashboard/users/styles.scss @@ -1,3 +1,8 @@ .panel-wrapper { margin-top: 12px; } + +.updated-at { + float: right; + margin: 12px 12px 0 0; +} diff --git a/app/institutions/dashboard/users/template.hbs b/app/institutions/dashboard/users/template.hbs index 9bcdf36be8f..3539dfbbbb6 100644 --- a/app/institutions/dashboard/users/template.hbs +++ b/app/institutions/dashboard/users/template.hbs @@ -1,5 +1,13 @@ + {{#if this.model.userMetric.reportYearmonth}} +
+ {{t 'institutions.dashboard.updated' date=this.model.userMetric.reportYearmonth}} +
+ {{/if}}
& SearchResultModel[]; diff --git a/app/models/institution-summary-metric.ts b/app/models/institution-summary-metric.ts index 9e84ede5b0d..36c48e0e150 100644 --- a/app/models/institution-summary-metric.ts +++ b/app/models/institution-summary-metric.ts @@ -13,7 +13,7 @@ export default class InstitutionSummaryMetricModel extends OsfModel { @attr('number') publicFileCount!: number; @attr('number') monthlyLoggedInUserCount!: number; @attr('number') monthlyActiveUserCount!: number; - + @attr('string') reportYearmonth!: string; get convertedStorageCount(): string { return humanFileSize(parseFloat(this.storageByteCount.toFixed(1))); diff --git a/app/models/institution-user.ts b/app/models/institution-user.ts index 2fb19f778d3..02836b918be 100644 --- a/app/models/institution-user.ts +++ b/app/models/institution-user.ts @@ -18,6 +18,7 @@ export default class InstitutionUserModel extends OsfModel { @attr('string') monthLastLogin!: string; // YYYY-MM @attr('string') monthLastActive!: string; // YYYY-MM @attr('string') accountCreationDate!: string; // YYYY-MM + @attr('string') reportYearmonth!: string; @attr('fixstring') orcidId?: string; @belongsTo('user', { async: true }) diff --git a/app/serializers/index-card-search.ts b/app/serializers/index-card-search.ts index 5b86f0c5600..49a0e7514ad 100644 --- a/app/serializers/index-card-search.ts +++ b/app/serializers/index-card-search.ts @@ -1,6 +1,20 @@ +import * as JSONAPI from 'jsonapi-typescript'; + import ShareSerializer from './share-serializer'; export default class IndexCardSearchSerializer extends ShareSerializer { + // Taken from osf-serializer.ts + _mergeLinks(resourceHash: JSONAPI.ResourceObject): Partial { + const links = { ...(resourceHash.links || {}) }; + return { + attributes: { ...resourceHash.attributes, links: (links as any) }, + }; + } + + extractAttributes(modelClass: any, resourceHash: JSONAPI.ResourceObject) { + const attributeHash = this._mergeLinks(resourceHash); + return super.extractAttributes(modelClass, attributeHash); + } } declare module 'ember-data/types/registries/serializer' { diff --git a/config/environment.js b/config/environment.js index 56f12bae31a..fcf4697d22e 100644 --- a/config/environment.js +++ b/config/environment.js @@ -315,6 +315,7 @@ module.exports = function(environment) { }, registrationFilesPage: 'ember_registration_files_page', egapAdmins: 'egap_admins', + shareDownload: 'share_download', }, gReCaptcha: { siteKey: RECAPTCHA_SITE_KEY, diff --git a/lib/osf-components/addon/components/index-card-searcher/component.ts b/lib/osf-components/addon/components/index-card-searcher/component.ts index c0b36fc6672..c2e69d870b6 100644 --- a/lib/osf-components/addon/components/index-card-searcher/component.ts +++ b/lib/osf-components/addon/components/index-card-searcher/component.ts @@ -9,6 +9,7 @@ import Toast from 'ember-toastr/services/toast'; import SearchResultModel from 'ember-osf-web/models/search-result'; import { taskFor } from 'ember-concurrency-ts'; import RelatedPropertyPathModel, { SuggestedFilterOperators } from 'ember-osf-web/models/related-property-path'; +import IndexCardSearchModel from 'ember-osf-web/models/index-card-search'; interface IndexCardSearcherArgs { queryOptions: Record; @@ -27,6 +28,7 @@ export default class IndexCardSearcher extends Component @tracked relatedProperties?: RelatedPropertyPathModel[] = []; @tracked booleanFilters?: RelatedPropertyPathModel[] = []; + @tracked latestIndexCardSearch?: IndexCardSearchModel; @tracked firstPageCursor?: string; @tracked nextPageCursor?: string; @tracked prevPageCursor?: string; @@ -64,6 +66,7 @@ export default class IndexCardSearcher extends Component (property: RelatedPropertyPathModel) => property.suggestedFilterOperator !== SuggestedFilterOperators.IsPresent, // AnyOf or AtDate ); + this.latestIndexCardSearch = searchResult; this.firstPageCursor = searchResult.firstPageCursor; this.nextPageCursor = searchResult.nextPageCursor; this.prevPageCursor = searchResult.prevPageCursor; diff --git a/lib/osf-components/addon/components/index-card-searcher/template.hbs b/lib/osf-components/addon/components/index-card-searcher/template.hbs index 76d1db68bd9..2b04d5b9799 100644 --- a/lib/osf-components/addon/components/index-card-searcher/template.hbs +++ b/lib/osf-components/addon/components/index-card-searcher/template.hbs @@ -7,6 +7,7 @@ debouceSearchObjectsTask=this.debouceSearchObjectsTask searchObjectsTask=this.searchObjectsTask + latestIndexCardSearch=this.latestIndexCardSearch firstPageCursor=this.firstPageCursor nextPageCursor=this.nextPageCursor prevPageCursor=this.prevPageCursor diff --git a/mirage/factories/institution-summary-metric.ts b/mirage/factories/institution-summary-metric.ts index 123c422d7d7..63fa25f6e43 100644 --- a/mirage/factories/institution-summary-metric.ts +++ b/mirage/factories/institution-summary-metric.ts @@ -34,6 +34,9 @@ export default Factory.extend({ monthlyLoggedInUserCount() { return faker.random.number({ min: 10, max: 100 * 100 }); }, + reportYearmonth() { + return faker.date.past(1).toISOString().slice(0, 7); + }, }); declare module 'ember-cli-mirage/types/registries/schema' { diff --git a/mirage/factories/institution-user.ts b/mirage/factories/institution-user.ts index beac8d9db6d..2820898dc24 100644 --- a/mirage/factories/institution-user.ts +++ b/mirage/factories/institution-user.ts @@ -44,6 +44,9 @@ export default Factory.extend({ orcidId() { return faker.random.uuid(); // Simulate an ORCID ID }, + reportYearmonth() { + return faker.date.past(1).toISOString().slice(0, 7); + }, afterCreate(institutionUser, server) { if (!institutionUser.userName && !institutionUser.userGuid) { const user = server.create('user'); diff --git a/mirage/factories/root.ts b/mirage/factories/root.ts index 3eacbabd108..ad859e2b6af 100644 --- a/mirage/factories/root.ts +++ b/mirage/factories/root.ts @@ -10,6 +10,7 @@ const { navigation, storageI18n, verifyEmailModals, + shareDownload, }, } = config; @@ -35,6 +36,7 @@ export const defaultRootAttrs = { ...Object.values(navigation), storageI18n, verifyEmailModals, + shareDownload, ])], message: 'Welcome to the OSF API.', version: '2.8', diff --git a/mirage/views/search.ts b/mirage/views/search.ts index c0723c3b47e..d2db1a48d80 100644 --- a/mirage/views/search.ts +++ b/mirage/views/search.ts @@ -277,10 +277,11 @@ export function cardSearch(_: Schema, request: Request) { requestedResourceTypes = Object.keys(resourceMetadataByType) as OsfmapResourceTypes[]; } + const indexCardSearchId = faker.random.uuid(); const indexCardSearch = { data: { type: 'index-card-search', - id: faker.random.uuid(), + id:indexCardSearchId, attributes: { cardSearchText: 'hello', cardSearchFilter: [ @@ -325,6 +326,9 @@ export function cardSearch(_: Schema, request: Request) { }, searchResultPage: {}, }, + links: { + self: `https://share.osf.io/api/v2/index-card-search/${indexCardSearchId}`, + }, }, included: [ // Related properties diff --git a/tests/acceptance/institutions/dashboard-test.ts b/tests/acceptance/institutions/dashboard-test.ts index 419ff7f172d..14d5d976aee 100644 --- a/tests/acceptance/institutions/dashboard-test.ts +++ b/tests/acceptance/institutions/dashboard-test.ts @@ -31,6 +31,7 @@ module(moduleName, hooks => { // Summary tab await percySnapshot(`${moduleName} - summary`); assert.dom('[data-test-page-tab="summary"]').hasClass('active', 'Summary tab is active by default'); + assert.dom('[data-test-summary-report-year-month]').exists('Report year month exists'); // Users tab await click('[data-test-page-tab="users"]'); @@ -38,6 +39,7 @@ module(moduleName, hooks => { assert.dom('[data-test-page-tab="users"]').hasClass('active', 'Users tab is active'); assert.dom('[data-test-link-to-reports-archive]').exists('Link to download prior reports exists'); assert.dom('[data-test-download-dropdown]').exists('Link to download file formats'); + assert.dom('[data-test-user-report-year-month]').exists('User report year month exists'); // Projects tab await click('[data-test-page-tab="projects"]'); diff --git a/tests/integration/routes/institutions/dashboard/-components/institutional-users-list/component-test.ts b/tests/integration/routes/institutions/dashboard/-components/institutional-users-list/component-test.ts index 7ea72714ede..6faf08a6885 100644 --- a/tests/integration/routes/institutions/dashboard/-components/institutional-users-list/component-test.ts +++ b/tests/integration/routes/institutions/dashboard/-components/institutional-users-list/component-test.ts @@ -35,10 +35,10 @@ module('Integration | routes | institutions | dashboard | -components | institut this.set('model', model); await render(hbs` - + `); assert.dom('[data-test-header]') .exists({ count: 9 }, '9 default headers'); @@ -70,6 +70,12 @@ module('Integration | routes | institutions | dashboard | -components | institut .exists({ count: 5 }, '5 in the list with public project'); assert.dom('[data-test-item="privateProjects"]') .exists({ count: 5 }, '5 in the list with private projects'); + + // Test download buttons + await click('[data-test-download-dropdown]'); + assert.dom('[data-test-csv-download-button]').exists('CSV download button'); + assert.dom('[data-test-tsv-download-button]').exists('TSV download button'); + assert.dom('[data-test-json-download-button]').exists('JSON download button'); }); test('it sorts', async function(assert) { @@ -105,10 +111,10 @@ module('Integration | routes | institutions | dashboard | -components | institut this.set('model', model); await render(hbs` - + `); assert.dom('[data-test-item="user_name"]') .exists({ count: 3 }, '3 users'); diff --git a/translations/en-us.yml b/translations/en-us.yml index 0416c52097d..79d39529dd6 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -811,6 +811,7 @@ institutions: preprints: preprints content-placeholder: Content coming soon # Delete this eventually pls title: '{institutionName} Dashboard' + updated: 'Updated {date}' download_past_reports_label: 'Previous reports' download_dropdown_label: 'Download data' download_csv: 'Download CSV' @@ -818,8 +819,7 @@ institutions: format_labels: csv: '.csv' tsv: '.tsv' - json_table: 'JSON (table)' - json_direct: 'JSON (direct)' + json: 'JSON' download: 'Download' select_default: 'All Departments' users_list: @@ -830,7 +830,7 @@ institutions: private_projects: 'Private Projects' empty: 'No users found matching search criteria.' public_registration_count: 'Public Registrations' - private_registration_count: 'Private Registrations' + embargoed_registration_count: 'Embargoed Registrations' published_preprint_count: 'Preprints' orcid: ORCID osf_link: 'OSF Link' @@ -861,10 +861,10 @@ institutions: title: 'Public vs Private Projects' public: 'Public Projects' private: 'Private Projects' - public-vs-private-registrations: - title: 'Public vs Private Registrations' + public-vs-embargoed-registrations: + title: 'Public vs Embargoed Registrations' public: 'Public Registrations' - private: 'Private Registrations' + embargoed: 'Embargoed Registrations' total-osf-objects: title: 'Total OSF Objects' preprints: 'Preprints' From df5b5441e3169356e67e7ef12a0407020e40bd16 Mon Sep 17 00:00:00 2001 From: Matt Frazier Date: Wed, 11 Dec 2024 13:19:19 -0500 Subject: [PATCH 2/2] Update CHANGELOG, bump version --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f0dc1f0ab8..f2ff18a28b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [24.10.0] - 2024-12-11 +### Added +- Institutional Dashboard Improvements B&I Release + ## [24.09.0] - 2024-11-21 ### Added - Institutional Dashboard Improvements Project - FE Release @@ -2006,6 +2010,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added - Quick Files +[24.10.0]: https://github.com/CenterForOpenScience/ember-osf-web/releases/tag/24.10.0 [24.09.1]: https://github.com/CenterForOpenScience/ember-osf-web/releases/tag/24.09.1 [24.09.0]: https://github.com/CenterForOpenScience/ember-osf-web/releases/tag/24.09.0 [24.08.0]: https://github.com/CenterForOpenScience/ember-osf-web/releases/tag/24.08.0 diff --git a/package.json b/package.json index e8e854e8b49..253e9ddaddb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ember-osf-web", - "version": "24.09.1", + "version": "24.10.0", "private": true, "description": "Ember front-end for the Open Science Framework", "homepage": "https://github.com/CenterForOpenScience/ember-osf-web#readme",