Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[POC/WIP] Angular component with redux settings #1944

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/assets/javascripts/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,4 @@
//= require rx-angular/dist/rx.angular
//= require patternfly-timeline/dist/timeline
//= require ui-select/dist/select
//= require ng-redux/dist/ng-redux
1 change: 1 addition & 0 deletions app/assets/javascripts/miq_angular_application.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ ManageIQ.angular.app = angular.module('ManageIQ', [
'miq.card',
'miq.util',
'kubernetesUI',
'ngRedux',
'miqStaticAssets.dialogEditor',
]);
miqHttpInject(ManageIQ.angular.app);
Expand Down
2 changes: 2 additions & 0 deletions app/javascript/custom-typings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
declare var ManageIQ: any;
declare var Rx: any;
68 changes: 68 additions & 0 deletions app/javascript/extensible-components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { IExtensionComponent, IMiQApiCallback, IMiQRenderCallback } from './lib';

const source = new Rx.Subject();
const items = new Set();

/**
* Class for easy creation of extensible component.
*/
export class ExtensibleComponent {
public delete: () => void;
constructor(public name: string, public api: IMiQApiCallback, public render: IMiQRenderCallback){}
}

/**
* Helper function to create new component.
* @param name string name of new component.
* @param api callback functions to change inner logic of component.
* @param render callback function to apply render functions.
*/
function addComponent(name: string, api?: IMiQApiCallback, render?: IMiQRenderCallback) {
let newCmp = new ExtensibleComponent(name, api, render);
source.onNext({action: 'add', payload: newCmp});
return newCmp;
}

/**
* Helper function to subscribe to extension items based on component's name.
* @param cmpName name of component we want to subscribe to.
* @return object which has with and unsubscribe property. With is for callback to use found component and delete to
* unsubscribe from rxjs subject.
*/
function subscribe(cmpName: string) {
let unsubscribe;
return {
with: (callback) => {
unsubscribe = source
.map(sourceAction => sourceAction.action === 'add' ? sourceAction.payload : {})
.filter(component => component && component.name === cmpName)
.subscribe(cmp => cmp && callback(cmp));

ManageIQ.extensions.items.forEach((component) => {
component.name === cmpName && callback(component);
});
},
delete: () => unsubscribe && unsubscribe.dispose()
}
}

const extensions = {
addComponent,
subscribe,
get items() { return items; }
}

ManageIQ.extensions = ManageIQ.extensions || extensions;

/**
* Subscribe to extensions source to add new components to items object.
*/
source.subscribe((component: IExtensionComponent) => {
if (component.action === 'add' && component.hasOwnProperty('payload')) {
component.payload.delete = () => ManageIQ.extensions.items.delete(component.payload);
ManageIQ.extensions.items.add(component.payload);
} else {
console.error('Unsupported action with extension components.');
}
});

37 changes: 37 additions & 0 deletions app/javascript/extensible-components/lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export type RenderCallback = (element: HTMLElement) => void;
export interface IExtensionComponent {
action: string;
payload: any;
}

export interface IMiQApiCallback {
[propName: string]: Function;
}

export interface IMiQRenderCallback {
[propName: string]: (renderCallback: RenderCallback) => void;
}

export interface IExtensibleComponent {
extensibleComponent: any;
apiCallbacks: () => IMiQApiCallback;
renderCallbacks: () => IMiQRenderCallback;
}

export function getItems() {
return ManageIQ.extensions.items;
}

export function subscribe(cmpName: string) {
return ManageIQ.extensions.subscribe(cmpName);
}

/**
* Helper function to create new component.
* @param name string name of new component.
* @param api callback functions to change inner logic of component.
* @param render callback function to apply render functions.
*/
export function addComponent(name: string, api?: IMiQApiCallback, render?: IMiQApiCallback) {
return ManageIQ.extensions.addComponent(name, api, render);
}
67 changes: 67 additions & 0 deletions app/javascript/forms-common/defaultFormController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { getStore, addReducer, applyReducerHash, UPDATE_FORM, INIT_FORM } from '../miq-redux/lib';
import { MiqStore, IMiqReducerHash, AppState, Action } from '../miq-redux/redux-types';

export interface IUnbindReduxReducers {
redux?: () => void;
reducer?: () => void;
}

export interface IFormController {
mapStateToThis: (state: AppState) => any;
}

export abstract class DefaultFormController {
protected unbind: IUnbindReduxReducers = {};
protected reduxStore: any;
public formObject: any;

/**
* Constructor which will get reduxStore and subscribes to it.
* @param reducersHash optional, hash of reducers which will be added to rootReducer.
*/
constructor(reducersHash?: IMiqReducerHash, protected Actions?) {
if (reducersHash) {
this.unbind.reducer = addReducer(
(state: AppState, action: Action) => applyReducerHash(reducersHash, state, action)
);
}
this.reduxStore = getStore();
this.initForm();
this.unbind.redux = this.reduxStore.connect(this.mapStateToThis, Actions)(this);
this.reduxStore.subscribe(() => this.refreshForm());
}

protected refreshForm() {}

protected mapStateToThis(state: AppState): any {
throw new Error('Controller should implement mapStateToThis method');
}

public updateForm(payload) {
throw new Error('Controller should implement updateForm method, did you forget to import it?');
}

public initForm() {
this.reduxStore.dispatch({type: INIT_FORM});
}

/**
* Method which is fired when component is destroyed.
* It will unbind redux from current scope and if some reducers were passed in, it will unbind them from reducer
* array as well.
*/
public $onDestroy(): void {
this.unbind.redux();
if (this.unbind.reducer) {
this.unbind.reducer();
}
}

/**
* Method which takes care of firing action `UPDATE_FORM`.
* It will pass formObject as payload to it.
*/
public onChangeForm(): void {
this.updateForm(this.formObject);
}
}
5 changes: 5 additions & 0 deletions app/javascript/middleware/forms/formActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { UPDATE_FORM, INIT_FORM } from '../../miq-redux/lib'

export function updateForm(payload) {
return dispatch => dispatch({type: UPDATE_FORM, payload: payload});
}
3 changes: 3 additions & 0 deletions app/javascript/middleware/forms/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import NewProviderForm from './new';

ManageIQ.angular.app.component('newProviderForm', new NewProviderForm());
39 changes: 39 additions & 0 deletions app/javascript/middleware/forms/new-provider-reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { UPDATE_FORM, INIT_FORM } from '../../miq-redux/lib';
import { AppState, IMiqReducerHash } from '../../miq-redux/redux-types';
import { merge, defaultsDeep } from 'lodash';

function initNewProvider(state, action): AppState {
const newProvider = {
providers: {
middleware: {
hawkular: {
newProvider: {
type: undefined,
zone: 'default',
protocol: undefined
}
}
}
}
};
return { ...defaultsDeep(state, newProvider) }
}

function updateNewProvider(state, action): AppState {
const newProvider = {
providers: {
middleware: {
hawkular: {
newProvider: {...action.payload}
}
}
}
};

return { ...merge(state, newProvider) }
}

export const reducers: IMiqReducerHash = {
[INIT_FORM]: initNewProvider,
[UPDATE_FORM]: updateNewProvider
};
89 changes: 89 additions & 0 deletions app/javascript/middleware/forms/new.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import * as ng from 'angular';
import { reducers } from './new-provider-reducer';
import { DefaultFormController, IFormController } from '../../forms-common/defaultFormController';
import {
IExtensibleComponent,
IMiQApiCallback,
RenderCallback,
IMiQRenderCallback,
addComponent
} from '../../extensible-components/lib';
import { ExtensibleComponent } from '../../extensible-components';
import * as FormActions from './formActions';

export default class NewProviderForm implements ng.IComponentOptions {
public templateUrl: string = '/static/middleware/new-provider.html.haml';
public controller: any = NewProviderController;
public controllerAs: string = 'newProv';
public bindings: any = {
types: '<',
zones: '<',
formFieldsUrl: '@',
novalidate: '@',
createUrl: '@'
};
}

class NewProviderController extends DefaultFormController implements IFormController, IExtensibleComponent {
public extensibleComponent: ExtensibleComponent;
public zones: any;
public types: any[];
public formObject: any;
private formFieldsUrl: string;
private novalidate: boolean;
private createUrl: string;
private selects: NodeListOf<HTMLSelectElement>;
public protocols = [
['<Choose>', undefined],
['SSL', 'ssl-with-validation'],
['SSL trusting custom CA', 'ssl-with-validation-custom-ca'],
['SSL without validation', 'ssl-with-validation'],
['Non-SSL', 'non-ssl'],
];

public static $inject = ['$element', '$scope', '$timeout'];

constructor(private $element: Element,
private $scope: ng.IScope,
private $timeout: ng.ITimeoutService) {
super(reducers, FormActions);
this.extensibleComponent = addComponent('new-provider-hawkular', this.apiCallbacks(), this.renderCallbacks());
}

public mapStateToThis(state) {
return {
formObject: state.providers.middleware.hawkular.newProvider
}
}

public refreshForm() {
this.$timeout(() => {
this.$scope.$apply();
(<any>angular.element(this.selects)).selectpicker('refresh');
});
}

public $onInit() {
this.selects = this.$element.querySelectorAll('select');
this.refreshForm();
}

public $onDestroy() {
super.$onDestroy();
this.extensibleComponent.delete();
}

public apiCallbacks(): IMiQApiCallback {
return {}
}

public renderCallbacks(): IMiQRenderCallback {
return {
newFieldsElement: (renderCallback) => this.newField(renderCallback)
}
}

private newField(renderCallback: RenderCallback) {
this.$timeout(() => renderCallback(angular.element(this.$element).find('.form-group.additional-fields')[0]));
}
}
13 changes: 13 additions & 0 deletions app/javascript/miq-redux/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { addReducer, applyReducerHash } from './reducer';
import { configureStore } from './store';
import { MiqStore } from './redux-types';

ManageIQ.redux = {};

const store: MiqStore = configureStore({});

ManageIQ.redux.addReducer = addReducer;

ManageIQ.redux.store = store;

ManageIQ.redux.applyReducerHash = applyReducerHash;
15 changes: 15 additions & 0 deletions app/javascript/miq-redux/lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export function addReducer(reducer) {
return ManageIQ.redux.addReducer(reducer);
}

export function getStore() {
return ManageIQ.redux.store;
}

export function applyReducerHash(reducers, state, action) {
return ManageIQ.redux.applyReducerHash(reducers, state, action);
}

export const UPDATE_FORM = 'UPDATE_FORM';

export const INIT_FORM = 'INIT_FORM';
25 changes: 25 additions & 0 deletions app/javascript/miq-redux/reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { createStore, Store, Reducer, Action } from 'redux';

import { IMiqAction, AppReducer, IMiqReducerHash, AppState } from './redux-types';

const reducers: Set<AppReducer> = new Set();

export const rootReducer: AppReducer = (state = {}, action: IMiqAction) => {
let newState = state;

reducers.forEach((reducer) => {
newState = reducer(newState, action)
});
return newState;
};

export function addReducer(reducer: AppReducer) {
reducers.add(reducer);
return () => {
reducers.delete(reducer);
}
}

export function applyReducerHash(reducers: IMiqReducerHash, state: AppState, action: IMiqAction): AppState {
return (reducers.hasOwnProperty(action.type) && reducers[action.type](state, action)) || state;
}
Loading