@nova-ui/dashboards
Version:
Nova Dashboards is a framework designed to provide feature developers with a common solution for presenting data coming from various sources within a single view, as well as a set of predefined widget visualizations that are 100% configuration-driven and
341 lines • 15.5 kB
JavaScript
"use strict";
// © 2022 SolarWinds Worldwide, LLC. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
Object.defineProperty(exports, "__esModule", { value: true });
exports.ComponentPortalDirective = void 0;
const tslib_1 = require("tslib");
const core_1 = require("@angular/core");
const isArray_1 = tslib_1.__importDefault(require("lodash/isArray"));
const values_1 = tslib_1.__importDefault(require("lodash/values"));
const xor_1 = tslib_1.__importDefault(require("lodash/xor"));
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const bits_1 = require("@nova-ui/bits");
const provider_registry_service_1 = require("../../../services/provider-registry.service");
const component_portal_service_1 = require("../../services/component-portal.service");
let ComponentPortalDirective = class ComponentPortalDirective {
constructor(injector, logger, portalService, renderer, providerRegistry) {
this.injector = injector;
this.logger = logger;
this.portalService = portalService;
this.renderer = renderer;
this.providerRegistry = providerRegistry;
this.output = new core_1.EventEmitter();
this.propertiesChanges = new rxjs_1.ReplaySubject();
this.providerInstances = {};
this.destroy$ = new rxjs_1.Subject();
}
ngOnInit() {
this.logger.debug("Portal created:", this.componentId);
this.recreatePortal();
}
ngAfterViewInit() {
Object.keys(this.providerInstances).forEach((key) => {
const provider = this.providerInstances[key];
if (provider.ngAfterViewInit) {
provider.ngAfterViewInit();
}
});
}
ngOnChanges(changes) {
let recreated = false;
const providersChange = changes.providers;
if (providersChange && !providersChange.isFirstChange()) {
if (this.checkForProviderChanges(providersChange)) {
this.logger.debug("Portal recreated (provider change):", this.componentId);
this.recreatePortal();
recreated = true; // we want to prevent multiple recreations within one ngOnChanges cycle
}
else {
this.updateProviderConfigurations(providersChange);
}
}
if (changes.componentType &&
!changes.componentType.isFirstChange() &&
!recreated) {
this.logger.debug("Portal recreated (component change):", this.componentId);
this.recreatePortal();
}
if (changes.properties) {
this.propertiesChanges.next(changes.properties);
}
}
ngOnDestroy() {
this.logger.debug("Portal destroyed:", this.componentId);
this.destroyProviders();
this.listenerUnsubscriber?.();
this.destroy$.next();
this.destroy$.complete();
}
attached(componentRef) {
this.component = componentRef.instance;
// ctrl+shift+click on a pizzagna to get a dump of its properties
// this will propagate through the parent components, so it should dump the parent pizzagnas as well
this.listenerUnsubscriber = this.renderer.listen(componentRef.location.nativeElement, "click", (event) => {
// event.metaKey is a CMD key for Mac
if ((event.ctrlKey || event.metaKey) &&
event.shiftKey &&
event.button === 0) {
console.log("==================== PIZZAGNA DUMP =========================");
console.log("componentId:", this.componentId);
console.log("component:", this.component);
console.log("componentType:", this.componentType);
console.log("properties:", this.properties);
console.log("providers:", this.providers);
console.log("providerInstances:", this.providerInstances);
console.log("============================================================");
}
});
for (const key of Object.keys(this.providerInstances)) {
const provider = this.providerInstances[key];
if (provider.setComponent) {
// setting componentId separately because "componentId" is used as another input,
// at the moment of setting the component here, 'this.component' doesn't have 'componentId' property,
// because 'changesSubscription' has not received any changes. it leads to the issue
// when providers do not know the ID until component property changed
provider.setComponent(this.component, this.componentId);
}
if (provider.setProviders) {
provider.setProviders(this.providerInstances, this.componentId);
}
}
this.subscribeToOutputs();
this.changesSubscription = this.propertiesChanges
.pipe((0, operators_1.takeUntil)(this.destroy$))
.subscribe((change) => this.applyPropertiesChange(change));
}
recreatePortal() {
if (this.changesSubscription) {
this.changesSubscription.unsubscribe();
this.changesSubscription = undefined;
this.propertiesChanges = new rxjs_1.ReplaySubject();
}
let injector = this.injector;
this.destroyProviders();
if (this.providers) {
injector = this.updateProviders(injector);
}
this.portal = this.portalService.createComponentPortal(this.componentType, injector);
// apply initial property values to the freshly (re)created component
if (this.properties) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const key of Object.keys(this.properties)) {
this.propertiesChanges.next(new core_1.SimpleChange(undefined, this.properties, true));
}
}
}
updateProviders(injector) {
const providerKeys = Object.keys(this.providers);
const registeredProviders = providerKeys.reduce((acc, next) => {
// for cases when providerId is just empty string, null or undefined (for example adding a tile doesn't provide a DS to it)
if (this.providers[next].providerId) {
acc[next] = this.providerRegistry.getProvider(this.providers[next].providerId);
}
return acc;
}, {});
const portalInjector = this.portalService.createInjector({
injector: injector,
providers: (0, values_1.default)(registeredProviders),
});
this.providerInstances = providerKeys.reduce((acc, next) => {
const registeredProvider = registeredProviders[next];
const providerConfig = this.providers[next];
if (registeredProvider) {
// TODO: Why do we pass null to the injectFlags parameter?
// TODO: How to retrieve info about the provider options to inject?InjectOptions
const injections = portalInjector.get(registeredProvider.provide, null);
const providerInstance = (0, isArray_1.default)(injections)
? injections.find((v) => v instanceof registeredProvider.useClass)
: injections;
if (!providerInstance) {
throw new Error("Provider '" + next + "' does not exist.");
}
if (providerInstance.updateConfiguration &&
providerConfig.properties) {
providerInstance.updateConfiguration({
providerKey: next,
...(providerConfig.properties || {}),
});
}
providerInstance.providerKey = next;
acc[next] = providerInstance;
}
else if (providerConfig.providerId) {
this.logger.error("There's no registered provider for id", next);
}
return acc;
}, this.providerInstances);
for (const key of Object.keys(this.providerInstances)) {
const pi = this.providerInstances[key];
if (pi.ngOnInit) {
pi.ngOnInit();
}
}
return portalInjector;
}
updateProviderConfigurations(providersChange) {
for (const providerKey of Object.keys(this.providers)) {
// for cases when providerId is just empty string, null or undefined (for example adding a tile doesn't provide a DS to it)
if (!this.providers[providerKey].providerId) {
return;
}
const providerInstance = this.providerInstances[providerKey];
if (!providerInstance) {
throw new Error("Provider '" + providerKey + "' does not exist.");
}
const previousValue = providersChange.previousValue[providerKey];
const currentValue = providersChange.currentValue[providerKey];
const previousProperties = previousValue && previousValue.properties;
const currentProperties = currentValue && currentValue.properties;
if (previousProperties !== currentProperties &&
providerInstance.updateConfiguration) {
providerInstance.updateConfiguration({
providerKey,
...currentProperties,
});
}
}
}
subscribeToOutputs() {
if (!this.outputs) {
return;
}
for (const output of this.outputs.filter((o) => this.component[o] instanceof core_1.EventEmitter)) {
this.component[output]
.pipe((0, operators_1.takeUntil)(this.destroy$))
.subscribe(($event) => {
this.output.emit({
id: output,
payload: $event,
});
});
}
}
checkForProviderChanges(change) {
const previousKeys = change.previousValue
? Object.keys(change.previousValue)
: [];
const currentKeys = change.currentValue
? Object.keys(change.currentValue)
: [];
if (previousKeys.length !== currentKeys.length ||
(0, xor_1.default)(previousKeys, currentKeys).length > 0) {
return true;
}
const previousIds = Object.keys(change.previousValue).map((key) => change.previousValue[key].providerId);
const currentIds = Object.keys(change.currentValue).map((key) => change.currentValue[key].providerId);
if ((0, xor_1.default)(previousIds, currentIds).length > 0) {
return true;
}
return false;
}
destroyProviders() {
if (this.providerInstances) {
for (const piKey of Object.keys(this.providerInstances)) {
const providerInstance = this.providerInstances[piKey];
if (providerInstance.ngOnDestroy) {
providerInstance.ngOnDestroy();
}
delete this.providerInstances[piKey];
}
}
}
/**
* There was a change in component properties, so we need to handle that here
*
* @param change a change of the `properties` input
*/
applyPropertiesChange(change) {
let properties = change.currentValue;
if (!properties) {
return;
}
// we add componentId to the mix, so components will be able to use it
properties = {
...properties,
componentId: this.componentId,
};
const changes = {};
let changeHappened = false;
for (const key of Object.keys(properties)) {
const previousValue = this.component[key];
// reference based change detection in action here!
if (properties[key] !== previousValue) {
changeHappened = true;
this.component[key] = properties[key];
// we're simulating angular by calling ngOnChanges manually
if (this.component.ngOnChanges) {
changes[key] = new core_1.SimpleChange(previousValue, properties[key], change.isFirstChange());
}
}
}
if (changeHappened) {
if (this.component.ngOnChanges) {
this.component.ngOnChanges(changes);
}
const changeDetector = this.component.changeDetector;
if (changeDetector) {
changeDetector.markForCheck();
}
else {
this.logger.warn(`Property 'changeDetector' is missing on component '${this.componentType}',` +
` so dynamic refresh of given component might not work.`);
}
}
}
};
exports.ComponentPortalDirective = ComponentPortalDirective;
tslib_1.__decorate([
(0, core_1.Input)(),
tslib_1.__metadata("design:type", String)
], ComponentPortalDirective.prototype, "componentId", void 0);
tslib_1.__decorate([
(0, core_1.Input)(),
tslib_1.__metadata("design:type", Object)
], ComponentPortalDirective.prototype, "componentType", void 0);
tslib_1.__decorate([
(0, core_1.Input)(),
tslib_1.__metadata("design:type", Object)
], ComponentPortalDirective.prototype, "properties", void 0);
tslib_1.__decorate([
(0, core_1.Input)(),
tslib_1.__metadata("design:type", Object)
], ComponentPortalDirective.prototype, "providers", void 0);
tslib_1.__decorate([
(0, core_1.Input)(),
tslib_1.__metadata("design:type", Array)
], ComponentPortalDirective.prototype, "outputs", void 0);
tslib_1.__decorate([
(0, core_1.Output)(),
tslib_1.__metadata("design:type", Object)
], ComponentPortalDirective.prototype, "output", void 0);
exports.ComponentPortalDirective = ComponentPortalDirective = tslib_1.__decorate([
(0, core_1.Directive)({
selector: "[nuiComponentPortal]",
exportAs: "nuiComponentPortal",
standalone: false,
}),
tslib_1.__metadata("design:paramtypes", [core_1.Injector,
bits_1.LoggerService,
component_portal_service_1.ComponentPortalService,
core_1.Renderer2,
provider_registry_service_1.ProviderRegistryService])
], ComponentPortalDirective);
//# sourceMappingURL=component-portal.directive.js.map