• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CenterForOpenScience / ember-osf-web / 14961996214

12 May 2025 01:41AM UTC coverage: 67.409%. First build
14961996214

Pull #2560

github

web-flow
Merge a35d27536 into 542d661ce
Pull Request #2560: [ENG-7576] Linked service settings page

3249 of 5300 branches covered (61.3%)

Branch coverage included in aggregate %.

18 of 60 new or added lines in 7 files covered. (30.0%)

8499 of 12128 relevant lines covered (70.08%)

185.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

54.65
/lib/osf-components/addon/components/addons-service/manager/component.ts
1
import EmberArray, { A } from '@ember/array';
2
import { action } from '@ember/object';
3
import { inject as service } from '@ember/service';
4
import { waitFor } from '@ember/test-waiters';
5
import Store from '@ember-data/store';
6
import Component from '@glimmer/component';
7
import { tracked } from '@glimmer/tracking';
8
import { Task, task } from 'ember-concurrency';
9
import { taskFor } from 'ember-concurrency-ts';
10
import IntlService from 'ember-intl/services/intl';
11
import Toast from 'ember-toastr/services/toast';
12
import { TrackedObject } from 'tracked-built-ins';
13

14
import ResourceReferenceModel from 'ember-osf-web/models/resource-reference';
15
import NodeModel from 'ember-osf-web/models/node';
16
import Provider, {
17
    AllAuthorizedAccountTypes, AllConfiguredAddonTypes,
18
} from 'ember-osf-web/packages/addons-service/provider';
19
import CurrentUserService from 'ember-osf-web/services/current-user';
20
import { ConfiguredAddonEditableAttrs } from 'ember-osf-web/models/configured-addon';
21
import ConfiguredStorageAddonModel from 'ember-osf-web/models/configured-storage-addon';
22
import { AccountCreationArgs} from 'ember-osf-web/models/authorized-account';
23
import AuthorizedStorageAccountModel from 'ember-osf-web/models/authorized-storage-account';
24
import ConfiguredCitationAddonModel from 'ember-osf-web/models/configured-citation-addon';
25
import UserReferenceModel from 'ember-osf-web/models/user-reference';
26
import ConfiguredLinkAddonModel from 'ember-osf-web/models/configured-link-addon';
27

28
interface FilterSpecificObject {
29
    modelName: string;
30
    task: Task<any, any>;
31
    list: EmberArray<Provider>;
32
    configuredAddons?: EmberArray<AllConfiguredAddonTypes>;
33
}
34

35
enum PageMode {
36
    TERMS = 'terms',
37
    NEW_OR_EXISTING_ACCOUNT = 'newOrExistingAccount',
38
    ACCOUNT_SELECT = 'accountSelect',
39
    ACCOUNT_CREATE = 'accountCreate',
40
    CONFIRM = 'confirm',
41
    CONFIGURE = 'configure',
42
    CONFIGURATION_LIST = 'configurationList'
43
}
44

45
export enum FilterTypes {
46
    STORAGE = 'additional-storage',
47
    CITATION_MANAGER = 'citation-manager',
48
    VERIFIED_LINK = 'verified-link',
49
    // CLOUD_COMPUTING = 'cloud-computing', // disabled because BOA is down
50
}
51

52
interface Args {
53
    node: NodeModel;
54
    activeFilterType: FilterTypes;
55
    updateActiveFilterType: (type: string) => void;
56
    updateTabIndex: (type: number) => void;
57
}
58

59
export default class AddonsServiceManagerComponent extends Component<Args> {
60
    @service store!: Store;
61
    @service currentUser!: CurrentUserService;
62
    @service intl!: IntlService;
63
    @service toast!: Toast;
64

65
    node = this.args.node;
4✔
66
    @tracked addonServiceNode?: ResourceReferenceModel | null;
67
    @tracked userReference?: UserReferenceModel;
68

69
    possibleFilterTypes = Object.values(FilterTypes);
4✔
70
    mapper: Record<FilterTypes, FilterSpecificObject> = {
4✔
71
        [FilterTypes.STORAGE]: {
72
            modelName: 'external-storage-service',
73
            task: taskFor(this.getStorageAddonProviders),
74
            list: A([]),
75
            configuredAddons: A([]),
76
        },
77
        [FilterTypes.CITATION_MANAGER]: {
78
            modelName: 'external-citation-service',
79
            task: taskFor(this.getCitationAddonProviders),
80
            list: A([]),
81
            configuredAddons: A([]),
82
        },
83
        [FilterTypes.VERIFIED_LINK]: {
84
            modelName: 'external-link-service',
85
            task: taskFor(this.getLinkAddonProviders),
86
            list: A([]),
87
            configuredAddons: A([]),
88
        },
89
        // [FilterTypes.CLOUD_COMPUTING]: {
90
        //     modelName: 'external-computing-service',
91
        //     task: taskFor(this.getComputingAddonProviders),
92
        //     list: A([]),
93
        //     configuredAddons: A([]),
94
        // },
95
    };
96
    filterTypeMapper = new TrackedObject(this.mapper);
4✔
97
    @tracked filterText = '';
4✔
98

99
    @tracked confirmRemoveConnectedLocation = false;
×
100
    @tracked _pageMode?: PageMode;
101
    @tracked _pageModeHistory: PageMode[] = [];
×
102
    @tracked selectedProvider?: Provider;
103
    @tracked selectedConfiguration?: AllConfiguredAddonTypes;
104
    @tracked selectedAccount?: AllAuthorizedAccountTypes;
105

106
    @action
107
    filterByAddonType(type: FilterTypes) {
108
        if (this.args.activeFilterType !== type) {
3!
109
            this.filterText = '';
3✔
110
        }
111
        this.args.updateActiveFilterType(type);
3✔
112
        const activeFilterObject = this.filterTypeMapper[type];
3✔
113
        if (activeFilterObject.list.length === 0) {
3✔
114
            activeFilterObject.task.perform();
2✔
115
        }
116
    }
117
    get pageMode(): PageMode | undefined {
118
        return this._pageMode;
17✔
119
    }
120

121
    set pageMode(value: PageMode | undefined) {
122
        if (this._pageMode && value) {
6✔
123
            this._pageModeHistory.push(this._pageMode);
2✔
124
        }
125
        this._pageMode = value;
6✔
126
    }
127

128
    get filteredConfiguredProviders() {
129
        const activeFilterObject = this.filterTypeMapper[this.args.activeFilterType];
5✔
130
        const possibleProviders = activeFilterObject.list;
5✔
131
        const textFilteredAddons = possibleProviders.filter(
5✔
132
            (provider: any) => provider.provider.displayName.toLowerCase().includes(this.filterText.toLowerCase()),
45✔
133
        );
134

135
        const configuredProviders = textFilteredAddons.filter((provider: Provider) => provider.isConfigured);
38✔
136

137
        return configuredProviders;
5✔
138
    }
139

140
    get filteredAddonProviders() {
141
        const activeFilterObject = this.filterTypeMapper[this.args.activeFilterType];
13✔
142
        const possibleProviders = activeFilterObject.list;
13✔
143
        const textFilteredAddons = possibleProviders.filter(
13✔
144
            (provider: any) => provider.provider.displayName.toLowerCase().includes(this.filterText.toLowerCase()),
89✔
145
        );
146

147
        return textFilteredAddons;
13✔
148
    }
149

150
    get currentListIsLoading() {
151
        const activeFilterObject = this.filterTypeMapper[this.args.activeFilterType];
17✔
152
        return activeFilterObject.task.isRunning || taskFor(this.initialize).isRunning;
17✔
153
    }
154

155
    @action
156
    async configureProvider(provider: Provider, configuredAddon: AllConfiguredAddonTypes) {
157
        this.cancelSetup();
1✔
158
        this.selectedProvider = provider;
1✔
159
        this.selectedConfiguration = configuredAddon;
1✔
160
        this.pageMode = PageMode.CONFIGURE;
1✔
161
    }
162

163
    @action
164
    back() {
165
        const previousPageMode = this._pageModeHistory.pop();
×
166
        if (!previousPageMode) {
×
167
            this.cancelSetup();
×
168
            return;
×
169
        }
170
        this._pageMode = previousPageMode;
×
171
        switch (previousPageMode) {
×
172
        case PageMode.CONFIGURATION_LIST:
173
            this.selectedProvider = this.selectedProvider || undefined;
×
174
            break;
×
175
        case PageMode.CONFIGURE:
176
            if (!this.selectedProvider || !this.selectedConfiguration) {
×
177
                this.cancelSetup();
×
178
            }
179
            break;
×
180
        case PageMode.TERMS:
181
            if (!this.selectedProvider) {
×
182
                this.cancelSetup();
×
183
            }
184
            break;
×
185
        default:
186
            break;
×
187
        }
188
    }
189

190

191
    @action
192
    listProviderConfigurations(provider: Provider) {
193
        this.cancelSetup();
2✔
194
        this.selectedProvider = provider;
2✔
195
        this.pageMode = PageMode.CONFIGURATION_LIST;
2✔
196
    }
197

198
    @action
199
    beginAccountSetup(provider: Provider) {
200
        this.cancelSetup();
1✔
201
        this.pageMode = PageMode.TERMS;
1✔
202
        this.selectedProvider = provider;
1✔
203
    }
204

205
    @action
206
    async acceptTerms() {
207
        await taskFor(this.selectedProvider!.getAuthorizedAccounts).perform();
1✔
208
        if(this.selectedProvider!.authorizedAccounts!.length > 0){
1!
209
            this.pageMode = PageMode.NEW_OR_EXISTING_ACCOUNT;
×
210
        } else {
211
            this.pageMode = PageMode.ACCOUNT_CREATE;
1✔
212
        }
213
    }
214

215
    @action
216
    chooseExistingAccount() {
217
        this.pageMode = PageMode.ACCOUNT_SELECT;
×
218
    }
219

220
    @action
221
    createNewAccount() {
222
        this.pageMode = PageMode.ACCOUNT_CREATE;
×
223
    }
224

225
    @action
226
    authorizeSelectedAccount() {
227
        if (this.selectedAccount && this.selectedAccount.credentialsAvailable) {
×
228
            this.pageMode = PageMode.CONFIRM;
×
229
        } else {
230
            this.pageMode = PageMode.ACCOUNT_CREATE;
×
231
        }
232
    }
233

234
    @task
235
    @waitFor
236
    async createAuthorizedAccount(arg: AccountCreationArgs) {
237
        if (this.selectedProvider) {
1!
238
            const newAccount = await taskFor(this.selectedProvider.createAuthorizedAccount)
1✔
239
                .perform(arg);
240
            return newAccount;
1✔
241
        }
242
        return undefined;
×
243
    }
244

245
    @task
246
    @waitFor
247
    async createConfiguredAddon(newAccount: AllAuthorizedAccountTypes) {
248
        if (this.selectedProvider) {
1!
249
            this.selectedConfiguration = await taskFor(this.selectedProvider.createConfiguredAddon).perform(newAccount);
1✔
250
        }
251
    }
252

253
    @task
254
    @waitFor
255
    async connectAccount(arg: AccountCreationArgs) {
256
        if (this.selectedProvider) {
1!
257
            const newAccount = await taskFor(this.createAuthorizedAccount).perform(arg);
1✔
258
            if (newAccount) {
1!
259
                await taskFor(this.createConfiguredAddon).perform(newAccount);
1✔
260
                this.pageMode = PageMode.CONFIGURE;
1✔
261
            }
262
        }
263
    }
264

265
    @task
266
    @waitFor
267
    async oauthFlowRefocus(newAccount: AllAuthorizedAccountTypes): Promise<boolean> {
268
        await newAccount.reload();
×
269
        if (newAccount.credentialsAvailable) {
×
270
            await taskFor(this.selectedProvider!.getAuthorizedAccounts).perform();
×
271
            this.selectedAccount = undefined;
×
272
            this.chooseExistingAccount();
×
273
            return true;
×
274
        }
275
        return false;
×
276
    }
277

278
    @action
279
    confirmAccountSetup() {
280
        this.pageMode = PageMode.CONFIGURE;
×
281
    }
282

283
    @action
284
    cancelSetup() {
285
        this._pageMode = undefined;
6✔
286
        this._pageModeHistory = [];
6✔
287
        this.selectedProvider = undefined;
6✔
288
        this.selectedConfiguration = undefined;
6✔
289
        this.selectedAccount = undefined;
6✔
290
        this.confirmRemoveConnectedLocation = false;
6✔
291
        this.args.updateTabIndex(0);
6✔
292
    }
293

294
    @task
295
    @waitFor
296
    async saveOrCreateConfiguration(args: ConfiguredAddonEditableAttrs) {
297
        try {
2✔
298
            if (!this.selectedConfiguration && this.selectedProvider && this.selectedAccount) {
2!
299
                this.selectedConfiguration = await taskFor(this.selectedProvider.createConfiguredAddon)
×
300
                    .perform(this.selectedAccount);
301
            }
302

303
            if (this.selectedConfiguration && (
2!
304
                this.selectedConfiguration instanceof ConfiguredStorageAddonModel ||
305
                this.selectedConfiguration instanceof ConfiguredCitationAddonModel)
306
            ) {
307
                this.selectedConfiguration.rootFolder = (args as ConfiguredAddonEditableAttrs).rootFolder;
2✔
308
                this.selectedConfiguration.displayName = args.displayName;
2✔
309
                await this.selectedConfiguration.save();
2✔
310
                this.toast.success(this.intl.t('addons.configure.success', {
2✔
311
                    configurationName: this.selectedConfiguration.displayName,
312
                }));
NEW
313
            } else if (this.selectedConfiguration && this.selectedConfiguration instanceof ConfiguredLinkAddonModel) {
×
NEW
314
                this.selectedConfiguration.targetId = args.targetId;
×
NEW
315
                this.selectedConfiguration.resourceType = args.resourceType;
×
NEW
316
                await this.selectedConfiguration.save();
×
317
            }
318
            this.cancelSetup();
2✔
319
        } catch(e) {
320
            const baseMessage = this.intl.t('addons.configure.error', {
×
321
                configurationName: this.selectedConfiguration?.displayName,
322
            });
323
            if (e.errors && e.errors[0].detail) {
×
324
                const apiMessage = e.errors[0].detail;
×
325
                this.toast.error(`${baseMessage}: ${apiMessage}`);
×
326
            } else {
327
                this.toast.error(baseMessage);
×
328
            }
329

330
        }
331
    }
332

333
    @action
334
    selectAccount(account: AuthorizedStorageAccountModel) {
335
        this.selectedAccount = account;
×
336
    }
337

338
    constructor(owner: unknown, args: Args) {
339
        super(owner, args);
4✔
340
        taskFor(this.initialize).perform();
4✔
341
    }
342

343
    @task
344
    @waitFor
345
    async initialize() {
346
        await Promise.all([
4✔
347
            taskFor(this.getUserReference).perform(),
348
            taskFor(this.getServiceNode).perform(),
349
        ]);
350
        await taskFor(this.getStorageAddonProviders).perform();
4✔
351
        const activeFilterObject = this.filterTypeMapper[this.args.activeFilterType];
4✔
352
        if (activeFilterObject.list.length === 0) {
4!
353
            activeFilterObject.task.perform();
×
354
        }
355
    }
356

357
    @task
358
    @waitFor
359
    async getServiceNode() {
360
        const references = await this.store.query('resource-reference', {
4✔
361
            filter: {resource_uri: this.node.links.iri},
362
        });
363
        if (references) {
4!
364
            this.addonServiceNode = references.firstObject || null;
4!
365
        } else {
366
            this.addonServiceNode = null;
×
367
        }
368
    }
369

370
    @task
371
    @waitFor
372
    async getStorageAddonProviders() {
373
        const activeFilterObject = this.filterTypeMapper[FilterTypes.STORAGE];
4✔
374

375
        if (this.addonServiceNode) {
4!
376
            const configuredAddons = await this.addonServiceNode.configuredStorageAddons;
4✔
377
            activeFilterObject.configuredAddons = A(configuredAddons.toArray());
4✔
378
        }
379

380
        const serviceStorageProviders: Provider[] =
381
            await taskFor(this.getExternalProviders)
4✔
382
                .perform(activeFilterObject.modelName, activeFilterObject.configuredAddons);
383
        activeFilterObject.list = A(serviceStorageProviders.sort(this.providerSorter));
4✔
384
    }
385

386
    @task
387
    @waitFor
388
    async getComputingAddonProviders() {
389
        const activeFilterObject = this.filterTypeMapper[FilterTypes.CLOUD_COMPUTING];
×
390

391
        if (this.addonServiceNode) {
×
392
            const configuredAddons = await this.addonServiceNode.configuredComputingAddons;
×
393
            activeFilterObject.configuredAddons = A(configuredAddons.toArray());
×
394
        }
395

396
        const serviceComputingProviders: Provider[] =
397
            await taskFor(this.getExternalProviders)
×
398
                .perform(activeFilterObject.modelName, activeFilterObject.configuredAddons);
399
        activeFilterObject.list = serviceComputingProviders.sort(this.providerSorter);
×
400
    }
401

402
    @task
403
    @waitFor
404
    async getCitationAddonProviders() {
405
        const activeFilterObject = this.filterTypeMapper[FilterTypes.CITATION_MANAGER];
2✔
406

407
        if (this.addonServiceNode) {
2!
408
            const configuredAddons = await this.addonServiceNode.configuredCitationAddons;
2✔
409
            activeFilterObject.configuredAddons = A(configuredAddons.toArray());
2✔
410
        }
411

412
        const serviceCitationProviders: Provider[] =
413
            await taskFor(this.getExternalProviders)
2✔
414
                .perform(activeFilterObject.modelName, activeFilterObject.configuredAddons);
415
        activeFilterObject.list = serviceCitationProviders.sort(this.providerSorter);
2✔
416
    }
417

418
    @task
419
    @waitFor
420
    async getLinkAddonProviders() {
NEW
421
        const activeFilterObject = this.filterTypeMapper[FilterTypes.VERIFIED_LINK];
×
422

NEW
423
        if (this.addonServiceNode) {
×
NEW
424
            const configuredAddons = await this.addonServiceNode.configuredLinkAddons;
×
NEW
425
            activeFilterObject.configuredAddons = A(configuredAddons.toArray());
×
426
        }
427

428
        const serviceCitationProviders: Provider[] =
NEW
429
            await taskFor(this.getExternalProviders)
×
430
                .perform(activeFilterObject.modelName, activeFilterObject.configuredAddons);
NEW
431
        activeFilterObject.list = serviceCitationProviders.sort(this.providerSorter);
×
432
    }
433

434
    providerSorter(a: Provider, b: Provider) {
435
        return a.provider.displayName.localeCompare(b.provider.displayName);
74✔
436
    }
437

438
    get projectEnabledAddons(): ConfiguredStorageAddonModel[] {
439
        return this.serviceProjectEnabledAddons();
4✔
440
    }
441

442
    get headingText() {
443
        const providerName = this.selectedProvider?.provider.displayName;
11✔
444
        let heading;
445
        switch (this.pageMode) {
11!
446
        case PageMode.TERMS:
447
            heading = this.intl.t('addons.terms.heading', { providerName });
1✔
448
            break;
1✔
449
        case PageMode.NEW_OR_EXISTING_ACCOUNT:
450
            heading = this.intl.t('addons.accountSelect.heading', { providerName });
×
451
            break;
×
452
        case PageMode.ACCOUNT_CREATE:
453
            heading = this.intl.t('addons.accountSelect.new-account');
1✔
454
            break;
1✔
455
        case PageMode.ACCOUNT_SELECT:
456
            heading = this.intl.t('addons.accountSelect.existing-account');
×
457
            break;
×
458
        case PageMode.CONFIRM:
459
            heading = this.intl.t('addons.confirm.heading', { providerName });
×
460
            break;
×
461
        case PageMode.CONFIGURE:
462
        case PageMode.CONFIGURATION_LIST:
463
            heading = this.intl.t('addons.configure.heading', { providerName });
4✔
464
            break;
4✔
465
        default:
466
            heading = this.intl.t('addons.heading');
5✔
467
            break;
5✔
468
        }
469
        return heading;
11✔
470
    }
471
    @task
472
    @waitFor
473
    async getUserReference() {
474
        if (this.userReference){
4!
475
            return;
×
476
        }
477
        const { user } = this.currentUser;
4✔
478
        const userReferences = await this.store.query('user-reference', {
4✔
479
            filter: {user_uri: user?.links.iri?.toString()},
480
        });
481
        this.userReference = userReferences.firstObject;
4✔
482
    }
483
    // Service API Methods
484

485
    @task
486
    @waitFor
487
    async getExternalProviders(providerType: string, configuredAddons?: EmberArray<AllConfiguredAddonTypes>) {
488
        const serviceProviderModels = (await this.store.findAll(providerType)).toArray();
6✔
489
        const serviceProviders = [] as Provider[];
6✔
490
        for (const provider of serviceProviderModels) {
6✔
491
            serviceProviders.addObject(new Provider(
40✔
492
                provider, this.currentUser, this.node, configuredAddons, this.addonServiceNode, this.userReference,
493
            ));
494
        }
495
        return serviceProviders;
6✔
496
    }
497

498
    serviceProjectEnabledAddons() {
499
        return this.addonServiceNode?.get('configuredStorageAddons').toArray() || [];
4!
500
    }
501
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc