Skip to content

Commit

Permalink
[ENG-4642] Boa Add-on Project PR - Ember Part (CenterForOpenScience#2046
Browse files Browse the repository at this point in the history
)

* [WIP] first stab at Boa addon ux

 * make boa modal findable
 * save WIP on dataset select
 * futa wisdom attained/accepted
 * add action; fetch parents properly
 * enabled addons enabled
 * cleanups; fix closemodal

* Add mirage support for BoA files

* Type updates

* authenticate addon endpoint request

* Add a bit of error handling to mirage

* Change logic to populate a node addons

* Disable submit to boa if no dataset selected

* Add qp to boa upload link

* Add placeholder message to dataset select

* Update toast messages

* CR feedback

* Boa bugs (CenterForOpenScience#2047)

* Add file size to boa request

* Allow checked out boa file to be submitted

* Use sizeInt

* Circumvent promise-induced bugs (CenterForOpenScience#2049)

---------

Co-authored-by: Fitz Elliott <[email protected]>
Co-authored-by: Futa Ikeda <[email protected]>
Co-authored-by: futa-ikeda <[email protected]>
  • Loading branch information
4 people authored and bp-cos committed Jan 31, 2024
1 parent d9f8949 commit 3dbcd96
Show file tree
Hide file tree
Showing 18 changed files with 324 additions and 3 deletions.
1 change: 1 addition & 0 deletions app/guid-file/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export default class GuidFile extends Route {
};
this.set('headTags', this.metaTags.getHeadTags(metaTagsData));
this.headTagsService.collectHeadTags();
await taskFor(model.target.get('getEnabledAddons')).perform();
blocker.done();
}

Expand Down
7 changes: 6 additions & 1 deletion app/guid-file/template.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@
({{t 'general.version'}}: {{this.viewedVersion}})
{{/if}}
</h2>
<FileActionsMenu @item={{this.model}} @onDelete={{this.onDelete}} @allowRename={{false}} />
<FileActionsMenu
@item={{this.model}}
@onDelete={{this.onDelete}}
@allowRename={{false}}
@addonsEnabled={{this.model.fileModel.target.addonsEnabled}}
/>
</div>
</:header>
<:body>
Expand Down
1 change: 1 addition & 0 deletions app/guid-node/files/provider/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default class GuidNodeFilesProviderRoute extends Route.extend({}) {
@waitFor
async fileProviderTask(guidRouteModel: GuidRouteModel<NodeModel>, fileProviderId: string) {
const node = await guidRouteModel.taskInstance;
await taskFor(node.getEnabledAddons).perform();
try {
const fileProviders = await node.queryHasMany(
'files',
Expand Down
36 changes: 36 additions & 0 deletions app/models/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ import { computed } from '@ember/object';
import { alias, bool, equal, not } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import { htmlSafe } from '@ember/template';
import { tracked } from '@glimmer/tracking';
import { buildValidations, validator } from 'ember-cp-validations';
import Intl from 'ember-intl/services/intl';
import CedarMetadataRecordModel from 'ember-osf-web/models/cedar-metadata-record';

import config from 'ember-osf-web/config/environment';
import { task } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters';

import getRelatedHref from 'ember-osf-web/utils/get-related-href';

import AbstractNodeModel from 'ember-osf-web/models/abstract-node';
Expand All @@ -26,6 +31,13 @@ import RegistrationModel from './registration';
import SubjectModel from './subject';
import WikiModel from './wiki';

const {
OSF: {
apiUrl,
apiNamespace,
},
} = config;

const Validations = buildValidations({
title: [
validator('presence', true),
Expand Down Expand Up @@ -109,6 +121,10 @@ export default class NodeModel extends AbstractNodeModel.extend(Validations, Col
@attr('boolean') currentUserCanComment!: boolean;
@attr('boolean') wikiEnabled!: boolean;

// FE-only property to check enabled addons.
// null until getEnabledAddons has been called
@tracked addonsEnabled?: string[];

@hasMany('contributor', { inverse: 'node' })
contributors!: AsyncHasMany<ContributorModel> & ContributorModel[];

Expand Down Expand Up @@ -315,6 +331,26 @@ export default class NodeModel extends AbstractNodeModel.extend(Validations, Col

this.set('nodeLicense', props);
}

@task
@waitFor
async getEnabledAddons() {
const endpoint = `${apiUrl}/${apiNamespace}/nodes/${this.id}/addons/`;
const response = await this.currentUser.authenticatedAJAX({
url: endpoint,
type: 'GET',
headers: {
'Content-Type': 'application/json',
},
xhrFields: { withCredentials: true },
});
if (response.data) {
const addonList = response.data
.filter((addon: any) => addon.attributes.node_has_auth)
.map((addon: any) => addon.id);
this.set('addonsEnabled', addonList);
}
}
}

declare module 'ember-data/types/registries/model' {
Expand Down
8 changes: 8 additions & 0 deletions app/packages/files/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ export default abstract class File {
);
}

get isBoaFile() {
return this.fileModel.name.endsWith('.boa');
}

get providerIsOsfstorage() {
return this.fileModel.provider === 'osfstorage';
}

async createFolder(newFolderName: string) {
if (this.fileModel.isFolder) {
await this.fileModel.createFolder(newFolderName);
Expand Down
33 changes: 33 additions & 0 deletions lib/osf-components/addon/components/file-actions-menu/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { inject as service } from '@ember/service';
import Media from 'ember-responsive';
import File from 'ember-osf-web/packages/files/file';
import StorageManager from 'osf-components/components/storage-provider-manager/storage-manager/component';
import NodeModel from 'ember-osf-web/models/node';

interface Args {
item: File;
onDelete: () => void;
manager?: StorageManager; // No manager for file-detail page
addonsEnabled? : string[];
}

export default class FileActionsMenu extends Component<Args> {
Expand All @@ -19,6 +21,7 @@ export default class FileActionsMenu extends Component<Args> {
@tracked moveModalOpen = false;
@tracked useCopyModal = false;
@tracked renameModalOpen = false;
@tracked isSubmitToBoaModalOpen = false;

@action
closeDeleteModal() {
Expand All @@ -34,4 +37,34 @@ export default class FileActionsMenu extends Component<Args> {
openRenameModal() {
this.renameModalOpen = true;
}

@action
closeSubmitToBoaModal() {
this.isSubmitToBoaModalOpen = false;
}

@action
openSubmitToBoaModal() {
this.isSubmitToBoaModalOpen = true;
}

get isBoaEnabled() {
return this.args.addonsEnabled?.includes('boa');
}

get showSubmitToBoa() {
const { item, manager } = this.args;
if (item.providerIsOsfstorage && item.isBoaFile && this.isBoaEnabled) {
let userCanUploadToHere;
if (manager) {
userCanUploadToHere = manager.currentFolder.userCanUploadToHere;
} else {
const storage = (item.fileModel.target as unknown as NodeModel).get('storage');
const writableTarget = item.currentUserPermission === 'write' && !item.targetIsRegistration;
userCanUploadToHere = writableTarget && !storage.get('isOverStorageCap');
}
return userCanUploadToHere;
}
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { inject as service } from '@ember/service';
import { waitFor } from '@ember/test-waiters';
import Component from '@glimmer/component';
import { task } from 'ember-concurrency';
import IntlService from 'ember-intl/services/intl';
import File from 'ember-osf-web/packages/files/file';
import captureException, { getApiErrorMessage } from 'ember-osf-web/utils/capture-exception';
import config from 'ember-osf-web/config/environment';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

interface Args {
file: File;
isOpen: boolean;
closeModal: () => {};
}

export default class SubmitToBoaModal extends Component<Args> {
@service toast!: Toastr;
@service intl!: IntlService;
@tracked selectedDataset?: string;

datasets = [
'2022 Jan/Java',
'2022 Feb/Python',
'2021 Method Chains',
'2021 Aug/Python',
'2021 Aug/Kotlin (small)',
'2021 Aug/Kotlin',
'2021 Jan/ML-Verse',
'2020 August/Python-DS',
'2019 October/GitHub (small)',
'2019 October/GitHub (medium)',
'2019 October/GitHub',
'2015 September/GitHub',
'2013 September/SF (small)',
'2013 September/SF (medium)',
'2013 September/SF',
'2013 May/SF',
'2013 February/SF',
'2012 July/SF',
];

@action
onDatasetChange(newDataset: string) {
this.selectedDataset = newDataset;
}

@task
@waitFor
async confirmSubmitToBoa() {
try {
const file = this.args.file;
const fileModel = file.fileModel;
const parentFolder = await fileModel.get('parentFolder');
const grandparentFolder = await parentFolder.get('parentFolder');
const endpoint = config.OSF.url + 'api/v1/project/' + fileModel.target.get('id') + '/boa/submit-job/';
const uploadLink = new URL(parentFolder.get('links').upload as string);
uploadLink.searchParams.set('kind', 'file');
const payload = {
data: {
nodeId: fileModel.target.get('id'),
name: file.name,
materialized: fileModel.materializedPath,
sizeInt: fileModel.size,
links: {
download: file.links.download,
upload: file.links.upload,
},
},
parent: {
links: {
upload: uploadLink.toString(),
},
isAddonRoot: !grandparentFolder,
},
dataset: this.selectedDataset,
};
await this.args.file.currentUser.authenticatedAJAX({
url: endpoint,
type: 'POST',
data: JSON.stringify(payload),
xhrFields: { withCredentials: true },
headers: {
'Content-Type': 'application/json',
},
});

this.args.closeModal();
} catch (e) {
captureException(e);
const errorMessageKey = this.intl.t('osf-components.file-browser.submit_to_boa_fail',
{ fileName: this.args.file.name, htmlSafe: true }) as string;
this.toast.error(getApiErrorMessage(e), errorMessageKey);
return;
}

this.toast.success(
this.intl.t('osf-components.file-browser.submit_to_boa_success', { fileName: this.args.file.name }),
);
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<OsfDialog @isOpen={{@isOpen}} @onClose={{@closeModal}} as |dialog|>
<dialog.heading>
{{t 'osf-components.file-browser.submit_to_boa'}}
</dialog.heading>
<dialog.main>

<p>{{t 'osf-components.file-browser.boa_dataset_spiel'}}</p>
<PowerSelect
@options={{this.datasets}}
@selected={{this.selectedDataset}}
@placeholder={{t 'osf-components.file-browser.boa_dataset_select_placeholder'}}
@onChange={{this.onDatasetChange}}
as |dataset|
>
{{dataset}}
</PowerSelect>
<p>{{t 'osf-components.file-browser.confirm_submit_to_boa' fileName=@file.name}}</p>

</dialog.main>
<dialog.footer>
<Button
{{on 'click' (fn (mut @isOpen) false)}}
>
{{t 'general.cancel'}}
</Button>
<Button
@type='primary'
disabled={{or this.confirmSubmitToBoa.isRunning (not this.selectedDataset)}}
{{on 'click' (perform this.confirmSubmitToBoa)}}
>
{{t 'osf-components.file-browser.confirm_submit_to_boa_yes'}}
</Button>
</dialog.footer>
</OsfDialog>
22 changes: 22 additions & 0 deletions lib/osf-components/addon/components/file-actions-menu/template.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,31 @@
</Button>
{{/if}}
{{/if}}
{{#if this.showSubmitToBoa}}
<Button
@layout='fake-link'
data-test-submit-to-boa
data-analytics-name='Submit to Boa'
local-class='DropdownItem'
{{on 'click' (queue
dropdown.close
this.openSubmitToBoaModal
)}}
>
<FaIcon @icon='upload' />
{{t 'file_actions_menu.submit_to_boa'}}
</Button>
{{/if}}
</dropdown.content>
</ResponsiveDropdown>
<FileActionsMenu::DeleteModal @file={{@item}} @isOpen={{this.isDeleteModalOpen}} @closeModal={{this.closeDeleteModal}} @onDelete={{@onDelete}} />
{{#if this.showSubmitToBoa}}
<FileActionsMenu::SubmitToBoaModal
@file={{@item}}
@isOpen={{this.isSubmitToBoaModalOpen}}
@closeModal={{this.closeSubmitToBoaModal}}
/>
{{/if}}
{{#if @manager}}
<MoveFileModal
@isOpen={{this.moveModalOpen}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,13 @@
local-class='FileList__item__options'
>
{{#unless @manager.selectedFiles}}
<FileActionsMenu @item={{@item}} @onDelete={{@manager.reload}} @manager={{@manager}} @allowRename={{true}} />
<FileActionsMenu
@item={{@item}}
@onDelete={{@manager.reload}}
@manager={{@manager}}
@allowRename={{true}}
@addonsEnabled={{@manager.targetNode.addonsEnabled}}
/>
{{/unless}}
</div>
</li>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from 'osf-components/components/file-actions-menu/submit-to-boa-modal/component';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from 'osf-components/components/file-actions-menu/submit-to-boa-modal/template';
8 changes: 7 additions & 1 deletion mirage/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import config from 'ember-osf-web/config/environment';

import { createReviewAction } from 'ember-osf-web/mirage/views/review-action';
import { createResource, updateResource } from 'ember-osf-web/mirage/views/resource';
import { addonsList } from './views/addons';
import { getCitation } from './views/citation';
import { createCollectionSubmission, getCollectionSubmissions } from './views/collection-submission';
import { createSubmissionAction } from './views/collection-submission-action';
Expand Down Expand Up @@ -49,7 +50,7 @@ import { updatePassword } from './views/user-password';
import * as userSettings from './views/user-setting';
import * as wb from './views/wb';

const { OSF: { apiUrl, shareBaseUrl } } = config;
const { OSF: { apiUrl, shareBaseUrl, url: osfUrl } } = config;

export default function(this: Server) {
this.passthrough(); // pass through all requests on currrent domain
Expand All @@ -67,6 +68,10 @@ export default function(this: Server) {
this.get('/index-value-search', valueSearch);
// this.get('/index-card/:id', Detail);

this.urlPrefix = osfUrl;
this.namespace = '/api/v1/';
this.post('project/:id/boa/submit-job/', () => ({})); // submissions to BoA

this.urlPrefix = apiUrl;
this.namespace = '/v2';

Expand Down Expand Up @@ -111,6 +116,7 @@ export default function(this: Server) {
onCreate: createBibliographicContributor,
});

this.get('/nodes/:parentID/addons', addonsList);
this.get('/nodes/:parentID/files', nodeFileProviderList); // Node file providers list
this.get('/nodes/:parentID/files/:fileProviderId', nodeFilesListForProvider); // Node files list for file provider
this.get('/nodes/:parentID/files/:fileProviderId/:folderId', folderFilesList); // Node folder detail view
Expand Down
Loading

0 comments on commit 3dbcd96

Please sign in to comment.