Skip to content

Commit

Permalink
Ericbrehault/sc 3546/brightcove connector (#500)
Browse files Browse the repository at this point in the history
* hide non-selected cards when entering params

* Brighcove connector

* Persist connector params [sc-3420]

* fix
  • Loading branch information
ebrehault authored Jan 11, 2023
1 parent 0c4f283 commit 44c0d29
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 70 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@import 'variables';

pa-card {
margin-right: rhythm(4);
margin: rhythm(2);

&.selected ::ng-deep .pa-card-wrapper {
border-color: $color-primary-regular;
Expand Down
131 changes: 67 additions & 64 deletions apps/desktop/src/app/connectors/connectors.component.html
Original file line number Diff line number Diff line change
@@ -1,69 +1,72 @@
<div class="connectors-list">
<nde-connector
*ngFor="let connector of connectors"
[selected]="selectedConnector?.id === connector.id"
[title]="connector.title"
[logo]="connector.logo"
[description]="connector.description"
(selectConnector)="onSelectConnector(connector.id)"></nde-connector>
</div>
<form
*ngIf="!!fields && !!form"
[formGroup]="form"
qa="fields-form"
(ngSubmit)="validate()">
<div
*ngFor="let field of fields"
class="field">
<pa-input
*ngIf="field.type === 'text'"
[help]="field.help | translate"
[formControlName]="field.id">
{{ field.label | translate }}
</pa-input>

<div class="container">
<div class="connectors-list">
<nde-connector
*ngFor="let connector of connectors"
[selected]="selectedConnector?.id === connector.id"
[title]="connector.title"
[logo]="connector.logo"
[description]="connector.description"
[hidden]="!!selectedConnector && selectedConnector.id !== connector.id"
(selectConnector)="onSelectConnector(connector.id)"></nde-connector>
</div>
<form
*ngIf="!!fields && !!form"
[formGroup]="form"
qa="fields-form"
(ngSubmit)="validate()">
<div
*ngIf="field.type === 'select'"
class="select-field-container">
<pa-select
[formControlName]="field.id"
[label]="field.label">
<pa-option
*ngFor="let option of field.options"
[disabled]="option.disabled"
[value]="option.value">
{{ option.label }}
</pa-option>
</pa-select>
*ngFor="let field of fields"
class="field">
<pa-input
*ngIf="field.type === 'text'"
[help]="field.help | translate"
[formControlName]="field.id">
{{ field.label | translate }}
</pa-input>

<div
*ngIf="field.type === 'select'"
class="select-field-container">
<pa-select
[formControlName]="field.id"
[label]="field.label">
<pa-option
*ngFor="let option of field.options"
[disabled]="option.disabled"
[value]="option.value">
{{ option.label }}
</pa-option>
</pa-select>

<pa-button
*ngIf="field.canBeRefreshed"
icon="refresh"
[paTooltip]="'upload.refresh' | translate: { field: field.label }"
(click)="refreshField(field.id)">
{{ 'upload.refresh' | translate: { field: field.label } }}
</pa-button>
</div>

<nde-folder-upload
*ngIf="field.type === 'folder'"
[formControlName]="field.id"></nde-folder-upload>
</div>

<div class="buttons">
<pa-button
*ngIf="field.canBeRefreshed"
icon="refresh"
[paTooltip]="'upload.refresh' | translate: { field: field.label }"
(click)="refreshField(field.id)">
{{ 'upload.refresh' | translate: { field: field.label } }}
qa="cancel"
kind="inverted"
(click)="cancel.next()">
{{ 'generic.cancel' | translate }}
</pa-button>
<pa-button
*ngIf="!!fields && !!form"
qa="validate"
type="submit"
kind="primary"
[disabled]="form.invalid || form.pristine">
{{ (type === 'sources' ? 'generic.next' : 'Upload') | translate }}
</pa-button>
</div>

<nde-folder-upload
*ngIf="field.type === 'folder'"
[formControlName]="field.id"></nde-folder-upload>
</div>

<div class="buttons">
<pa-button
qa="cancel"
kind="inverted"
(click)="cancel.next()">
{{ 'generic.cancel' | translate }}
</pa-button>
<pa-button
*ngIf="!!fields && !!form"
qa="validate"
type="submit"
kind="primary"
[disabled]="form.invalid || form.pristine">
{{ (type === 'sources' ? 'generic.next' : 'Upload') | translate }}
</pa-button>
</div>
</form>
</form>
</div>
7 changes: 7 additions & 0 deletions apps/desktop/src/app/connectors/connectors.component.scss
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
@import 'variables';

.container {
display: flex;
}
.connectors-list {
display: flex;
flex-wrap: wrap;
padding: rhythm(1);
margin-bottom: rhythm(8);
}

form {
width: 100%;
}

.field {
margin-bottom: rhythm(2);
}
Expand Down
23 changes: 20 additions & 3 deletions apps/desktop/src/app/connectors/connectors.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ConnectorDefinition, ConnectorParameters, Field } from '../sync/models'
import { SyncService } from '../sync/sync.service';
import { markForCheck } from '@guillotinaweb/pastanaga-angular';

const PARAMS_CACHE = 'PARAMS_CACHE';
@Component({
selector: 'nde-connectors',
templateUrl: './connectors.component.html',
Expand Down Expand Up @@ -62,7 +63,7 @@ export class ConnectorsComponent {
)
.subscribe((fields) => {
fields.length > 0
? this.showFields(fields)
? this.showFields(connectorId, fields)
: this.selectedConnector && this.selectConnector.emit({ connector: this.selectedConnector, params: {} });
});
} else {
Expand All @@ -73,21 +74,26 @@ export class ConnectorsComponent {
take(1),
)
.subscribe((fields) => {
this.showFields(fields);
this.showFields(connectorId, fields);
});
}
}

showFields(fields: Field[]) {
showFields(connectorId: string, fields: Field[]) {
this.fields = fields;
this.form = this.formBuilder.group(
fields.reduce((acc, field) => ({ ...acc, [field.id]: ['', field.required ? [Validators.required] : []] }), {}),
);
const cache = this.getCache(connectorId);
if (cache) {
this.form.patchValue(cache);
}
markForCheck(this.cdr);
}

validate() {
if (this.selectedConnector) {
this.saveCache(this.selectedConnector.id, this.form?.value || {});
this.selectConnector.emit({ connector: this.selectedConnector, params: this.form?.value || {} });
}
}
Expand All @@ -107,4 +113,15 @@ export class ConnectorsComponent {
});
}
}

private getCache(connectorId: string): any {
const cache = localStorage.getItem(PARAMS_CACHE) || '{}';
return JSON.parse(cache)[connectorId];
}

private saveCache(connectorId: string, params: any) {
const cache = localStorage.getItem(PARAMS_CACHE) || '{}';
const parsedCache = JSON.parse(cache);
localStorage.setItem(PARAMS_CACHE, JSON.stringify({ ...parsedCache, [connectorId]: params }));
}
}
142 changes: 142 additions & 0 deletions apps/desktop/src/app/sync/sources/brightcove.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import {
ConnectorSettings,
ISourceConnector,
SourceConnectorDefinition,
SyncItem,
SearchResults,
ConnectorParameters,
FileStatus,
Field,
} from '../models';
import { Observable, of, from, map, switchMap } from 'rxjs';

const MAX_PAGE_SIZE = 1000;

export const BrightcoveConnector: SourceConnectorDefinition = {
id: 'brightcove',
title: 'Brightcove',
logo: 'assets/logos/brightcove.svg',
description: 'Video delivery platform',
factory: (data?: ConnectorSettings) => of(new BrightcoveImpl(data)),
};

class BrightcoveImpl implements ISourceConnector {
hasServerSideAuth = false;
isExternal = true;
resumable = false;
account = '';
client_id = '';
client_secret = '';

constructor(data?: ConnectorSettings) {
// eslint-disable-next-line no-empty-function
}

getParameters(): Observable<Field[]> {
return of([
{
id: 'account',
label: 'Account ID',
type: 'text',
required: true,
},
{
id: 'client_id',
label: 'Client ID',
type: 'text',
required: true,
},
{
id: 'client_secret',
label: 'Client secret',
type: 'text',
required: true,
},
]);
}

handleParameters(params: ConnectorParameters) {
this.account = params.account;
this.client_id = params.client_id;
this.client_secret = params.client_secret;
}

goToOAuth() {
// eslint-disable-next-line no-empty-function
}

authenticate(): Observable<boolean> {
return this.account && this.client_id && this.client_secret ? of(true) : of(false);
}

getFiles(query?: string, pageSize?: number): Observable<SearchResults> {
return this._getFiles(query, pageSize);
}

private _getFiles(query?: string, pageSize: number = 50, nextPage = 0): Observable<SearchResults> {
query = query ? `&query=${query}` : '';
return this.getToken().pipe(
switchMap((token) =>
from(
fetch(
`https://cms.api.brightcove.com/v1/accounts/${this.account}/videos?limit=${pageSize}&offset=${nextPage}${query}`,
{
method: 'GET',
headers: { Authorization: `Bearer ${token}` },
},
).then((res) => res.json()),
),
),
map((results: any[]) => ({
items: (results || []).map(this.mapResult),
nextPage:
results.length === 0 || results[0].error_code
? undefined
: this._getFiles(query, pageSize, nextPage + pageSize),
})),
);
}

private mapResult(result: any) {
return {
originalId: result.id || '',
title: result.name || '',
uuid: result.id,
metadata: {},
status: FileStatus.PENDING,
};
}

private getToken(): Observable<string> {
const credentials = `${this.client_id}:${this.client_secret}`;
return from(
fetch('https://oauth.brightcove.com/v4/access_token?grant_type=client_credentials', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Authorization: `Basic ${btoa(credentials)}` },
})
.then((response) => response.json())
.then((data) => data.access_token as string),
);
}

getLink(resource: SyncItem): Observable<{ uri: string; extra_headers: { [key: string]: string } }> {
return this.getToken().pipe(
switchMap((token) =>
from(
fetch(`https://cms.api.brightcove.com/v1/accounts/${this.account}/videos/${resource.originalId}/sources`, {
method: 'GET',
headers: { Authorization: `Bearer ${token}` },
}).then((res) => res.json()),
),
),
map((results: any[]) => {
const playlist = results.find((result: any) => result.src.includes('.m3u8'));
return playlist ? { uri: playlist.src, extra_headers: {} } : { uri: '', extra_headers: {} };
}),
);
}

download(resource: SyncItem): Observable<Blob> {
throw 'Error';
}
}
Loading

0 comments on commit 44c0d29

Please sign in to comment.