diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 90fcaa6d2..1d250a0d5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -34,7 +34,7 @@ jobs:
- name: Install (Prod dependencies only)
if: ${{ matrix.flavor == 'prod'}}
- run: npm ci --prod --no-optional
+ run: npm ci --omit=dev --omit=optional
working-directory: ${{ matrix.flavor }}/backend
- name: Lint
@@ -70,7 +70,7 @@ jobs:
registry-url: 'https://registry.npmjs.org'
- name: Install (Prod dependencies only)
- run: npm ci --prod --no-optional
+ run: npm ci --omit=dev --omit=optional
if: ${{ matrix.flavor == 'prod'}}
working-directory: ${{ matrix.flavor }}/frontend
diff --git a/docker/backend/Dockerfile b/docker/backend/Dockerfile
index 31acff047..6b368f59e 100644
--- a/docker/backend/Dockerfile
+++ b/docker/backend/Dockerfile
@@ -8,7 +8,7 @@ COPY . .
RUN apt-get update
RUN apt-get install -y build-essential python3 pkg-config
-RUN npm install
+RUN npm install --omit=dev --omit=optional
RUN npm run build
FROM node:16.15.0-buster-slim
diff --git a/docker/frontend/Dockerfile b/docker/frontend/Dockerfile
index e2874ff4e..b58b8ccd0 100644
--- a/docker/frontend/Dockerfile
+++ b/docker/frontend/Dockerfile
@@ -8,7 +8,7 @@ WORKDIR /build
COPY . .
RUN apt-get update
RUN apt-get install -y build-essential rsync
-RUN npm i
+RUN npm install --omit=dev --omit=optional
RUN npm run build
FROM nginx:1.17.8-alpine
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 1b3e98b8a..1dc5832fa 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -36,7 +36,6 @@
"echarts": "~5.3.2",
"express": "^4.17.1",
"lightweight-charts": "~3.8.0",
- "ngx-bootrap-multiselect": "^2.0.0",
"ngx-echarts": "8.0.1",
"ngx-infinite-scroll": "^10.0.1",
"qrcode": "1.5.0",
@@ -12788,19 +12787,6 @@
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
},
- "node_modules/ngx-bootrap-multiselect": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ngx-bootrap-multiselect/-/ngx-bootrap-multiselect-2.0.0.tgz",
- "integrity": "sha512-GV/2MigCS5oi6P+zWtFSmq1TLWW1kcKsJNAXLP3hHXxmY3HgMKeUPk57o3T+YHje73JRp5reXMhEIlYuoOmoRg==",
- "dependencies": {
- "tslib": "^2.0.0"
- },
- "peerDependencies": {
- "@angular/common": "^10.0.6",
- "@angular/core": "^10.0.6",
- "@angular/forms": "^10.0.6"
- }
- },
"node_modules/ngx-echarts": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/ngx-echarts/-/ngx-echarts-8.0.1.tgz",
@@ -27418,14 +27404,6 @@
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
},
- "ngx-bootrap-multiselect": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ngx-bootrap-multiselect/-/ngx-bootrap-multiselect-2.0.0.tgz",
- "integrity": "sha512-GV/2MigCS5oi6P+zWtFSmq1TLWW1kcKsJNAXLP3hHXxmY3HgMKeUPk57o3T+YHje73JRp5reXMhEIlYuoOmoRg==",
- "requires": {
- "tslib": "^2.0.0"
- }
- },
"ngx-echarts": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/ngx-echarts/-/ngx-echarts-8.0.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 6c626f3c9..573e2181f 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -90,7 +90,6 @@
"echarts": "~5.3.2",
"express": "^4.17.1",
"lightweight-charts": "~3.8.0",
- "ngx-bootrap-multiselect": "^2.0.0",
"ngx-echarts": "8.0.1",
"ngx-infinite-scroll": "^10.0.1",
"qrcode": "1.5.0",
diff --git a/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.html b/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.html
index 7a2056b46..8d34448d8 100644
--- a/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.html
+++ b/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.html
@@ -3,7 +3,7 @@
diff --git a/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.ts b/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.ts
index 0df156ad0..d0a2ba3c5 100644
--- a/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.ts
+++ b/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.ts
@@ -7,7 +7,7 @@ import { BisqApiService } from '../bisq-api.service';
import { SeoService } from 'src/app/services/seo.service';
import { FormGroup, FormBuilder } from '@angular/forms';
import { Router, ActivatedRoute } from '@angular/router';
-import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from 'ngx-bootrap-multiselect';
+import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from 'src/app/components/ngx-bootstrap-multiselect/types'
import { WebsocketService } from 'src/app/services/websocket.service';
@Component({
diff --git a/frontend/src/app/bisq/bisq.module.ts b/frontend/src/app/bisq/bisq.module.ts
index ebb9c3ee6..93658d95a 100644
--- a/frontend/src/app/bisq/bisq.module.ts
+++ b/frontend/src/app/bisq/bisq.module.ts
@@ -1,7 +1,6 @@
import { NgModule } from '@angular/core';
import { BisqRoutingModule } from './bisq.routing.module';
import { SharedModule } from '../shared/shared.module';
-import { NgxBootstrapMultiselectModule } from 'ngx-bootrap-multiselect';
import { LightweightChartsComponent } from './lightweight-charts/lightweight-charts.component';
import { LightweightChartsAreaComponent } from './lightweight-charts-area/lightweight-charts-area.component';
@@ -24,6 +23,10 @@ import { BisqStatsComponent } from './bisq-stats/bisq-stats.component';
import { BsqAmountComponent } from './bsq-amount/bsq-amount.component';
import { BisqTradesComponent } from './bisq-trades/bisq-trades.component';
import { CommonModule } from '@angular/common';
+import { AutofocusDirective } from '../components/ngx-bootstrap-multiselect/autofocus.directive';
+import { MultiSelectSearchFilter } from '../components/ngx-bootstrap-multiselect/search-filter.pipe';
+import { OffClickDirective } from '../components/ngx-bootstrap-multiselect/off-click.directive';
+import { NgxDropdownMultiselectComponent } from '../components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component';
@NgModule({
declarations: [
@@ -44,16 +47,21 @@ import { CommonModule } from '@angular/common';
BisqMarketComponent,
BisqTradesComponent,
BisqMainDashboardComponent,
+ NgxDropdownMultiselectComponent,
+ AutofocusDirective,
+ OffClickDirective,
],
imports: [
CommonModule,
BisqRoutingModule,
SharedModule,
FontAwesomeModule,
- NgxBootstrapMultiselectModule,
],
providers: [
BisqApiService,
+ MultiSelectSearchFilter,
+ AutofocusDirective,
+ OffClickDirective,
]
})
export class BisqModule {
diff --git a/frontend/src/app/components/ngx-bootstrap-multiselect/autofocus.directive.ts b/frontend/src/app/components/ngx-bootstrap-multiselect/autofocus.directive.ts
new file mode 100644
index 000000000..dcf21c884
--- /dev/null
+++ b/frontend/src/app/components/ngx-bootstrap-multiselect/autofocus.directive.ts
@@ -0,0 +1,41 @@
+import { Directive, ElementRef, Host, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
+
+@Directive({
+ selector: '[ssAutofocus]'
+})
+export class AutofocusDirective implements OnInit, OnChanges {
+
+ /**
+ * Will set focus if set to falsy value or not set at all
+ */
+ @Input() ssAutofocus: any;
+
+ get element(): { focus?: Function } {
+ return this.elemRef.nativeElement;
+ }
+
+ constructor(
+ @Host() private elemRef: ElementRef,
+ ) { }
+
+ ngOnInit() {
+ this.focus();
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ const ssAutofocusChange = changes.ssAutofocus;
+
+ if (ssAutofocusChange && !ssAutofocusChange.isFirstChange()) {
+ this.focus();
+ }
+ }
+
+ focus() {
+ if (this.ssAutofocus) {
+ return;
+ }
+
+ this.element.focus && this.element.focus();
+ }
+
+}
diff --git a/frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.css b/frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.css
new file mode 100644
index 000000000..fbaae90a7
--- /dev/null
+++ b/frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.css
@@ -0,0 +1,48 @@
+a {
+ outline: none !important;
+}
+
+.dropdown-inline {
+ display: inline-block;
+}
+
+.dropdown-toggle .caret {
+ margin-left: 4px;
+ white-space: nowrap;
+ display: inline-block;
+}
+
+.chunkydropdown-menu {
+ min-width: 20em;
+}
+
+.chunkyrow {
+ line-height: 2;
+ margin-left: 1em;
+ font-size: 2em;
+}
+
+.slider {
+ width:3.8em;
+ height:3.8em;
+ display:block;
+ -webkit-transition: all 0.125s linear;
+ -moz-transition: all 0.125s linear;
+ -o-transition: all 0.125s linear;
+ transition: all 0.125s linear;
+ margin-left: 0.125em;
+ margin-top: auto;
+}
+
+.slideron {
+ margin-left: 1.35em;
+}
+
+.content_wrapper{
+ display: table-cell;
+ vertical-align: middle;
+}
+
+.search-container {
+ padding: 0px 5px 5px 5px;
+}
diff --git a/frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.html b/frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.html
new file mode 100644
index 000000000..055f2789e
--- /dev/null
+++ b/frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.html
@@ -0,0 +1,72 @@
+
+
+
+
diff --git a/frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.ts b/frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.ts
new file mode 100644
index 000000000..50b195905
--- /dev/null
+++ b/frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.ts
@@ -0,0 +1,710 @@
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ DoCheck,
+ EventEmitter,
+ forwardRef,
+ Input,
+ IterableDiffers,
+ OnChanges,
+ OnDestroy,
+ OnInit,
+ Output,
+ SimpleChanges,
+} from '@angular/core';
+
+import {
+ AbstractControl,
+ ControlValueAccessor,
+ FormBuilder,
+ FormControl,
+ NG_VALUE_ACCESSOR,
+ Validator,
+} from '@angular/forms';
+
+import { takeUntil } from 'rxjs/operators';
+import { MultiSelectSearchFilter } from './search-filter.pipe';
+import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts, } from './types';
+import { Subject, Observable } from 'rxjs';
+
+const MULTISELECT_VALUE_ACCESSOR: any = {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => NgxDropdownMultiselectComponent),
+ multi: true,
+};
+
+// tslint:disable-next-line: no-conflicting-lifecycle
+@Component({
+ selector: 'ngx-bootstrap-multiselect',
+ templateUrl: './ngx-bootstrap-multiselect.component.html',
+ styleUrls: ['./ngx-bootstrap-multiselect.component.css'],
+ providers: [MULTISELECT_VALUE_ACCESSOR, MultiSelectSearchFilter],
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class NgxDropdownMultiselectComponent implements OnInit,
+ OnChanges,
+ DoCheck,
+ OnDestroy,
+ ControlValueAccessor,
+ Validator {
+
+ private localIsVisible = false;
+ private workerDocClicked = false;
+
+ filterControl: FormControl = this.fb.control('');
+
+ @Input() options: Array;
+ @Input() settings: IMultiSelectSettings;
+ @Input() texts: IMultiSelectTexts;
+ @Input() disabled = false;
+ @Input() disabledSelection = false;
+ @Input() searchFunction: (str: string) => RegExp = this._escapeRegExp;
+
+ @Output() selectionLimitReached = new EventEmitter();
+ @Output() dropdownClosed = new EventEmitter();
+ @Output() dropdownOpened = new EventEmitter();
+ @Output() added = new EventEmitter();
+ @Output() removed = new EventEmitter();
+ @Output() lazyLoad = new EventEmitter();
+ @Output() filter: Observable = this.filterControl.valueChanges;
+
+ get focusBack(): boolean {
+ return this.settings.focusBack && this._focusBack;
+ }
+
+ destroyed$ = new Subject();
+
+ filteredOptions: IMultiSelectOption[] = [];
+ lazyLoadOptions: IMultiSelectOption[] = [];
+ renderFilteredOptions: IMultiSelectOption[] = [];
+ model: any[] = [];
+ prevModel: any[] = [];
+ parents: any[];
+ title: string;
+ differ: any;
+ numSelected = 0;
+ set isVisible(val: boolean) {
+ this.localIsVisible = val;
+ this.workerDocClicked = val ? false : this.workerDocClicked;
+ }
+ get isVisible(): boolean {
+ return this.localIsVisible;
+ }
+ renderItems = true;
+ checkAllSearchRegister = new Set();
+ checkAllStatus = false;
+ loadedValueIds = [];
+ _focusBack = false;
+ focusedItem: IMultiSelectOption | undefined;
+
+ defaultSettings: IMultiSelectSettings = {
+ closeOnClickOutside: true,
+ pullRight: false,
+ enableSearch: false,
+ searchRenderLimit: 0,
+ searchRenderAfter: 1,
+ searchMaxLimit: 0,
+ searchMaxRenderedItems: 0,
+ checkedStyle: 'checkboxes',
+ buttonClasses: 'btn btn-primary dropdown-toggle',
+ containerClasses: 'dropdown-inline',
+ selectionLimit: 0,
+ minSelectionLimit: 0,
+ closeOnSelect: false,
+ autoUnselect: false,
+ showCheckAll: false,
+ showUncheckAll: false,
+ fixedTitle: false,
+ dynamicTitleMaxItems: 3,
+ maxHeight: '300px',
+ isLazyLoad: false,
+ stopScrollPropagation: false,
+ loadViewDistance: 1,
+ selectAddedValues: false,
+ ignoreLabels: false,
+ maintainSelectionOrderInTitle: false,
+ focusBack: true
+ };
+ defaultTexts: IMultiSelectTexts = {
+ checkAll: 'Select all',
+ uncheckAll: 'Unselect all',
+ checked: 'selected',
+ checkedPlural: 'selected',
+ searchPlaceholder: 'Search...',
+ searchEmptyResult: 'Nothing found...',
+ searchNoRenderText: 'Type in search box to see results...',
+ defaultTitle: 'Select',
+ allSelected: 'All selected',
+ };
+
+ get searchLimit(): number | undefined {
+ return this.settings.searchRenderLimit;
+ }
+
+ get searchRenderAfter(): number | undefined {
+ return this.settings.searchRenderAfter;
+ }
+
+ get searchLimitApplied(): boolean {
+ return this.searchLimit > 0 && this.options.length > this.searchLimit;
+ }
+
+ constructor(
+ private fb: FormBuilder,
+ private searchFilter: MultiSelectSearchFilter,
+ differs: IterableDiffers,
+ private cdRef: ChangeDetectorRef
+ ) {
+ this.differ = differs.find([]).create(null);
+ this.settings = this.defaultSettings;
+ this.texts = this.defaultTexts;
+ }
+
+ clickedOutside(): void {
+ if (!this.isVisible || !this.settings.closeOnClickOutside) { return; }
+
+ this.isVisible = false;
+ this._focusBack = true;
+ this.dropdownClosed.emit();
+ }
+
+ getItemStyle(option: IMultiSelectOption): any {
+ const style = {};
+ if (!option.isLabel) {
+ style['cursor'] = 'pointer';
+ }
+ if (option.disabled) {
+ style['cursor'] = 'default';
+ }
+ }
+
+ getItemStyleSelectionDisabled(): any {
+ if (this.disabledSelection) {
+ return { cursor: 'default' };
+ }
+ }
+
+ ngOnInit(): void {
+ this.title = this.texts.defaultTitle || '';
+
+ this.filterControl.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe(() => {
+ this.updateRenderItems();
+ if (this.settings.isLazyLoad) {
+ this.load();
+ }
+ });
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes['options']) {
+ this.options = this.options || [];
+ this.parents = this.options
+ .filter(option => typeof option.parentId === 'number')
+ .map(option => option.parentId);
+ this.updateRenderItems();
+
+ if (
+ this.settings.isLazyLoad &&
+ this.settings.selectAddedValues &&
+ this.loadedValueIds.length === 0
+ ) {
+ this.loadedValueIds = this.loadedValueIds.concat(
+ changes.options.currentValue.map(value => value.id)
+ );
+ }
+ if (
+ this.settings.isLazyLoad &&
+ this.settings.selectAddedValues &&
+ changes.options.previousValue
+ ) {
+ const addedValues = changes.options.currentValue.filter(
+ value => this.loadedValueIds.indexOf(value.id) === -1
+ );
+ this.loadedValueIds.concat(addedValues.map(value => value.id));
+ if (this.checkAllStatus) {
+ this.addChecks(addedValues);
+ } else if (this.checkAllSearchRegister.size > 0) {
+ this.checkAllSearchRegister.forEach((searchValue: string) =>
+ this.addChecks(this.applyFilters(addedValues, searchValue))
+ );
+ }
+ }
+
+ if (this.texts) {
+ this.updateTitle();
+ }
+
+ this.fireModelChange();
+ }
+
+ if (changes['settings']) {
+ this.settings = { ...this.defaultSettings, ...this.settings };
+ }
+
+ if (changes['texts']) {
+ this.texts = { ...this.defaultTexts, ...this.texts };
+ if (!changes['texts'].isFirstChange()) { this.updateTitle(); }
+ }
+ }
+
+ ngOnDestroy() {
+ this.destroyed$.next(false);
+ }
+
+ updateRenderItems() {
+ this.renderItems =
+ !this.searchLimitApplied ||
+ this.filterControl.value.length >= this.searchRenderAfter;
+ this.filteredOptions = this.applyFilters(
+ this.options,
+ this.settings.isLazyLoad ? '' : this.filterControl.value
+ );
+ this.renderFilteredOptions = this.renderItems ? this.filteredOptions : [];
+ this.focusedItem = undefined;
+ }
+
+ applyFilters(options: IMultiSelectOption[], value: string): IMultiSelectOption[] {
+ return this.searchFilter.transform(
+ options,
+ value,
+ this.settings.searchMaxLimit,
+ this.settings.searchMaxRenderedItems,
+ this.searchFunction
+ );
+ }
+
+ fireModelChange(): void {
+ if (this.model != this.prevModel) {
+ this.prevModel = this.model;
+ this.onModelChange(this.model);
+ this.onModelTouched();
+ this.cdRef.markForCheck();
+ }
+ }
+
+ onModelChange: Function = (_: any) => { };
+ onModelTouched: Function = () => { };
+
+ writeValue(value: any): void {
+ if (value !== undefined && value !== null) {
+ this.model = Array.isArray(value) ? value : [value];
+ this.ngDoCheck();
+ } else {
+ this.model = [];
+ }
+ }
+
+ registerOnChange(fn: Function): void {
+ this.onModelChange = fn;
+ }
+
+ registerOnTouched(fn: Function): void {
+ this.onModelTouched = fn;
+ }
+
+ setDisabledState(isDisabled: boolean) {
+ this.disabled = isDisabled;
+ }
+
+ ngDoCheck() {
+ const changes = this.differ.diff(this.model);
+ if (changes) {
+ this.updateNumSelected();
+ this.updateTitle();
+ }
+ }
+
+ validate(_c: AbstractControl): { [key: string]: any } {
+ if (this.model && this.model.length) {
+ return {
+ required: {
+ valid: false
+ }
+ };
+ }
+
+ if (this.options.filter(o => this.model.indexOf(o.id) && !o.disabled).length === 0) {
+ return {
+ selection: {
+ valid: false
+ }
+ };
+ }
+
+ return null;
+ }
+
+ registerOnValidatorChange(_fn: () => void): void {
+ throw new Error('Method not implemented.');
+ }
+
+ clearSearch(event: Event) {
+ this.maybeStopPropagation(event);
+ this.filterControl.setValue('');
+ }
+
+ toggleDropdown(e?: Event) {
+ if (this.isVisible) {
+ this._focusBack = true;
+ }
+
+ this.isVisible = !this.isVisible;
+ this.isVisible ? this.dropdownOpened.emit() : this.dropdownClosed.emit();
+ this.focusedItem = undefined;
+ }
+
+ closeDropdown(e?: Event) {
+ this.isVisible = true;
+ this.toggleDropdown(e);
+ }
+
+ isSelected(option: IMultiSelectOption): boolean {
+ return this.model && this.model.indexOf(option.id) > -1;
+ }
+
+ setSelected(_event: Event, option: IMultiSelectOption) {
+ if (option.isLabel) {
+ return;
+ }
+
+ if (option.disabled) {
+ return;
+ }
+
+ if (this.disabledSelection) {
+ return;
+ }
+
+ setTimeout(() => {
+ this.maybeStopPropagation(_event);
+ this.maybePreventDefault(_event);
+ const index = this.model.indexOf(option.id);
+ const isAtSelectionLimit =
+ this.settings.selectionLimit > 0 &&
+ this.model.length >= this.settings.selectionLimit;
+ const removeItem = (idx, id): void => {
+ this.model.splice(idx, 1);
+ this.removed.emit(id);
+ if (
+ this.settings.isLazyLoad &&
+ this.lazyLoadOptions.some(val => val.id === id)
+ ) {
+ this.lazyLoadOptions.splice(
+ this.lazyLoadOptions.indexOf(
+ this.lazyLoadOptions.find(val => val.id === id)
+ ),
+ 1
+ );
+ }
+ };
+
+ if (index > -1) {
+ if (
+ this.settings.minSelectionLimit === undefined ||
+ this.numSelected > this.settings.minSelectionLimit
+ ) {
+ removeItem(index, option.id);
+ }
+ const parentIndex =
+ option.parentId && this.model.indexOf(option.parentId);
+ if (parentIndex > -1) {
+ removeItem(parentIndex, option.parentId);
+ } else if (this.parents.indexOf(option.id) > -1) {
+ this.options
+ .filter(
+ child =>
+ this.model.indexOf(child.id) > -1 &&
+ child.parentId === option.id
+ )
+ .forEach(child =>
+ removeItem(this.model.indexOf(child.id), child.id)
+ );
+ }
+ } else if (isAtSelectionLimit && !this.settings.autoUnselect) {
+ this.selectionLimitReached.emit(this.model.length);
+ return;
+ } else {
+ const addItem = (id): void => {
+ this.model.push(id);
+ this.added.emit(id);
+ if (
+ this.settings.isLazyLoad &&
+ !this.lazyLoadOptions.some(val => val.id === id)
+ ) {
+ this.lazyLoadOptions.push(option);
+ }
+ };
+
+ addItem(option.id);
+ if (!isAtSelectionLimit) {
+ if (option.parentId && !this.settings.ignoreLabels) {
+ const children = this.options.filter(
+ child =>
+ child.id !== option.id && child.parentId === option.parentId
+ );
+ if (children.every(child => this.model.indexOf(child.id) > -1)) {
+ addItem(option.parentId);
+ }
+ } else if (this.parents.indexOf(option.id) > -1) {
+ const children = this.options.filter(
+ child =>
+ this.model.indexOf(child.id) < 0 && child.parentId === option.id
+ );
+ children.forEach(child => addItem(child.id));
+ }
+ } else {
+ removeItem(0, this.model[0]);
+ }
+ }
+ if (this.settings.closeOnSelect) {
+ this.toggleDropdown();
+ }
+ this.model = this.model.slice();
+ this.fireModelChange();
+
+ }, 0)
+ }
+
+ updateNumSelected() {
+ this.numSelected =
+ this.model.filter(id => this.parents.indexOf(id) < 0).length || 0;
+ }
+
+ updateTitle() {
+ let numSelectedOptions = this.options.length;
+ if (this.settings.ignoreLabels) {
+ numSelectedOptions = this.options.filter(
+ (option: IMultiSelectOption) => !option.isLabel
+ ).length;
+ }
+ if (this.numSelected === 0 || this.settings.fixedTitle) {
+ this.title = this.texts ? this.texts.defaultTitle : '';
+ } else if (
+ this.settings.displayAllSelectedText &&
+ this.model.length === numSelectedOptions
+ ) {
+ this.title = this.texts ? this.texts.allSelected : '';
+ } else if (
+ this.settings.dynamicTitleMaxItems &&
+ this.settings.dynamicTitleMaxItems >= this.numSelected
+ ) {
+ const useOptions =
+ this.settings.isLazyLoad && this.lazyLoadOptions.length
+ ? this.lazyLoadOptions
+ : this.options;
+
+ let titleSelections: Array;
+
+ if (this.settings.maintainSelectionOrderInTitle) {
+ const optionIds = useOptions.map((selectOption: IMultiSelectOption, idx: number) => selectOption.id);
+ titleSelections = this.model
+ .map((selectedId) => optionIds.indexOf(selectedId))
+ .filter((optionIndex) => optionIndex > -1)
+ .map((optionIndex) => useOptions[optionIndex]);
+ } else {
+ titleSelections = useOptions.filter((option: IMultiSelectOption) => this.model.indexOf(option.id) > -1);
+ }
+
+ this.title = titleSelections.map((option: IMultiSelectOption) => option.name).join(', ');
+ } else {
+ this.title =
+ this.numSelected +
+ ' ' +
+ (this.numSelected === 1
+ ? this.texts.checked
+ : this.texts.checkedPlural);
+ }
+ this.cdRef.markForCheck();
+ }
+
+ searchFilterApplied() {
+ return (
+ this.settings.enableSearch &&
+ this.filterControl.value &&
+ this.filterControl.value.length > 0
+ );
+ }
+
+ addChecks(options) {
+ const checkedOptions = options
+ .filter((option: IMultiSelectOption) => {
+ if (
+ !option.disabled &&
+ (
+ this.model.indexOf(option.id) === -1 &&
+ !(this.settings.ignoreLabels && option.isLabel)
+ )
+ ) {
+ this.added.emit(option.id);
+ return true;
+ }
+ return false;
+ })
+ .map((option: IMultiSelectOption) => option.id);
+
+ this.model = this.model.concat(checkedOptions);
+ }
+
+ checkAll(): void {
+ if (!this.disabledSelection) {
+ this.addChecks(
+ !this.searchFilterApplied() ? this.options : this.filteredOptions
+ );
+ if (this.settings.isLazyLoad && this.settings.selectAddedValues) {
+ if (this.searchFilterApplied() && !this.checkAllStatus) {
+ this.checkAllSearchRegister.add(this.filterControl.value);
+ } else {
+ this.checkAllSearchRegister.clear();
+ this.checkAllStatus = true;
+ }
+ this.load();
+ }
+ this.fireModelChange();
+ }
+ }
+
+ uncheckAll(): void {
+ if (!this.disabledSelection) {
+ const checkedOptions = this.model;
+ let unCheckedOptions = !this.searchFilterApplied()
+ ? this.model
+ : this.filteredOptions.map((option: IMultiSelectOption) => option.id);
+ // set unchecked options only to the ones that were checked
+ unCheckedOptions = checkedOptions.filter(item => unCheckedOptions.indexOf(item) > -1);
+ this.model = this.model.filter((id: number) => {
+ if (
+ (unCheckedOptions.indexOf(id) < 0 &&
+ this.settings.minSelectionLimit === undefined) ||
+ unCheckedOptions.indexOf(id) < this.settings.minSelectionLimit
+ ) {
+ return true;
+ } else {
+ this.removed.emit(id);
+ return false;
+ }
+ });
+ if (this.settings.isLazyLoad && this.settings.selectAddedValues) {
+ if (this.searchFilterApplied()) {
+ if (this.checkAllSearchRegister.has(this.filterControl.value)) {
+ this.checkAllSearchRegister.delete(this.filterControl.value);
+ this.checkAllSearchRegister.forEach(function(searchTerm) {
+ const filterOptions = this.applyFilters(this.options.filter(option => unCheckedOptions.indexOf(option.id) > -1), searchTerm);
+ this.addChecks(filterOptions);
+ });
+ }
+ } else {
+ this.checkAllSearchRegister.clear();
+ this.checkAllStatus = false;
+ }
+ this.load();
+ }
+ this.fireModelChange();
+ }
+ }
+
+ preventCheckboxCheck(event: Event, option: IMultiSelectOption): void {
+ if (
+ option.disabled ||
+ (
+ this.settings.selectionLimit &&
+ !this.settings.autoUnselect &&
+ this.model.length >= this.settings.selectionLimit &&
+ this.model.indexOf(option.id) === -1 &&
+ this.maybePreventDefault(event)
+ )
+ ) {
+ this.maybePreventDefault(event);
+ }
+ }
+
+ isCheckboxDisabled(option?: IMultiSelectOption): boolean {
+ return this.disabledSelection || option && option.disabled;
+ }
+
+ checkScrollPosition(ev): void {
+ const scrollTop = ev.target.scrollTop;
+ const scrollHeight = ev.target.scrollHeight;
+ const scrollElementHeight = ev.target.clientHeight;
+ const roundingPixel = 1;
+ const gutterPixel = 1;
+
+ if (
+ scrollTop >=
+ scrollHeight -
+ (1 + this.settings.loadViewDistance) * scrollElementHeight -
+ roundingPixel -
+ gutterPixel
+ ) {
+ this.load();
+ }
+ }
+
+ checkScrollPropagation(ev, element): void {
+ const scrollTop = element.scrollTop;
+ const scrollHeight = element.scrollHeight;
+ const scrollElementHeight = element.clientHeight;
+
+ if (
+ (ev.deltaY > 0 && scrollTop + scrollElementHeight >= scrollHeight) ||
+ (ev.deltaY < 0 && scrollTop <= 0)
+ ) {
+ ev = ev || window.event;
+ this.maybePreventDefault(ev);
+ ev.returnValue = false;
+ }
+ }
+
+ trackById(idx: number, selectOption: IMultiSelectOption): void {
+ return selectOption.id;
+ }
+
+ load(): void {
+ this.lazyLoad.emit({
+ length: this.options.length,
+ filter: this.filterControl.value,
+ checkAllSearches: this.checkAllSearchRegister,
+ checkAllStatus: this.checkAllStatus,
+ });
+ }
+
+ focusItem(dir: number, e?: Event): void {
+ if (!this.isVisible) {
+ return;
+ }
+
+ this.maybePreventDefault(e);
+
+ const idx = this.filteredOptions.indexOf(this.focusedItem);
+
+ if (idx === -1) {
+ this.focusedItem = this.filteredOptions[0];
+ return;
+ }
+
+ const nextIdx = idx + dir;
+ const newIdx =
+ nextIdx < 0
+ ? this.filteredOptions.length - 1
+ : nextIdx % this.filteredOptions.length;
+
+ this.focusedItem = this.filteredOptions[newIdx];
+ }
+
+ private maybePreventDefault(e?: Event): void {
+ if (e && e.preventDefault) {
+ e.preventDefault();
+ }
+ }
+
+ private maybeStopPropagation(e?: Event): void {
+ if (e && e.stopPropagation) {
+ e.stopPropagation();
+ }
+ }
+
+ private _escapeRegExp(str: string): RegExp {
+ const regExpStr = str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
+ return new RegExp(regExpStr, 'i');
+ }
+}
diff --git a/frontend/src/app/components/ngx-bootstrap-multiselect/off-click.directive.ts b/frontend/src/app/components/ngx-bootstrap-multiselect/off-click.directive.ts
new file mode 100644
index 000000000..6620c436c
--- /dev/null
+++ b/frontend/src/app/components/ngx-bootstrap-multiselect/off-click.directive.ts
@@ -0,0 +1,39 @@
+import { Directive, HostListener } from '@angular/core';
+import { EventEmitter } from '@angular/core';
+import { Output } from '@angular/core';
+
+@Directive({
+ // tslint:disable-next-line:directive-selector
+ selector: '[offClick]',
+})
+
+export class OffClickDirective {
+ @Output('offClick') onOffClick = new EventEmitter();
+
+ private _clickEvent: MouseEvent;
+ private _touchEvent: TouchEvent;
+
+ @HostListener('click', ['$event'])
+ public onClick(event: MouseEvent): void {
+ this._clickEvent = event;
+ }
+
+ @HostListener('touchstart', ['$event'])
+ public onTouch(event: TouchEvent): void {
+ this._touchEvent = event;
+ }
+
+ @HostListener('document:click', ['$event'])
+ public onDocumentClick(event: MouseEvent): void {
+ if (event !== this._clickEvent) {
+ this.onOffClick.emit(event);
+ }
+ }
+
+ @HostListener('document:touchstart', ['$event'])
+ public onDocumentTouch(event: TouchEvent): void {
+ if (event !== this._touchEvent) {
+ this.onOffClick.emit(event);
+ }
+ }
+}
diff --git a/frontend/src/app/components/ngx-bootstrap-multiselect/search-filter.pipe.ts b/frontend/src/app/components/ngx-bootstrap-multiselect/search-filter.pipe.ts
new file mode 100644
index 000000000..1dfb57ffd
--- /dev/null
+++ b/frontend/src/app/components/ngx-bootstrap-multiselect/search-filter.pipe.ts
@@ -0,0 +1,130 @@
+import { Pipe, PipeTransform } from '@angular/core';
+import { IMultiSelectOption } from './types';
+
+interface StringHashMap {
+ [k: string]: T;
+}
+
+@Pipe({
+ name: 'searchFilter'
+})
+export class MultiSelectSearchFilter implements PipeTransform {
+
+ private _lastOptions: IMultiSelectOption[];
+ private _searchCache: StringHashMap = {};
+ private _searchCacheInclusive: StringHashMap = {};
+ private _prevSkippedItems: StringHashMap = {};
+
+ transform(
+ options: IMultiSelectOption[],
+ str = '',
+ limit = 0,
+ renderLimit = 0,
+ searchFunction: (str: string) => RegExp,
+ ): IMultiSelectOption[] {
+ str = str.toLowerCase();
+
+ // Drop cache because options were updated
+ if (options !== this._lastOptions) {
+ this._lastOptions = options;
+ this._searchCache = {};
+ this._searchCacheInclusive = {};
+ this._prevSkippedItems = {};
+ }
+
+ const filteredOpts = this._searchCache.hasOwnProperty(str)
+ ? this._searchCache[str]
+ : this._doSearch(options, str, limit, searchFunction);
+
+ const isUnderLimit = options.length <= limit;
+
+ return isUnderLimit
+ ? filteredOpts
+ : this._limitRenderedItems(filteredOpts, renderLimit);
+ }
+
+ private _getSubsetOptions(
+ options: IMultiSelectOption[],
+ prevOptions: IMultiSelectOption[],
+ prevSearchStr: string
+ ) {
+ const prevInclusiveOrIdx = this._searchCacheInclusive[prevSearchStr];
+
+ if (prevInclusiveOrIdx === true) {
+ // If have previous results and it was inclusive, do only subsearch
+ return prevOptions;
+ } else if (typeof prevInclusiveOrIdx === 'number') {
+ // Or reuse prev results with unchecked ones
+ return [...prevOptions, ...options.slice(prevInclusiveOrIdx)];
+ }
+
+ return options;
+ }
+
+ private _doSearch(options: IMultiSelectOption[], str: string, limit: number, searchFunction: (str: string) => RegExp) {
+ const prevStr = str.slice(0, -1);
+ const prevResults = this._searchCache[prevStr];
+ const prevResultShift = this._prevSkippedItems[prevStr] || 0;
+
+ if (prevResults) {
+ options = this._getSubsetOptions(options, prevResults, prevStr);
+ }
+
+ const optsLength = options.length;
+ const maxFound = limit > 0 ? Math.min(limit, optsLength) : optsLength;
+ const regexp = searchFunction(str);
+ const filteredOpts: IMultiSelectOption[] = [];
+
+ let i = 0, founded = 0, removedFromPrevResult = 0;
+
+ const doesOptionMatch = (option: IMultiSelectOption) => regexp.test(option.name);
+ const getChildren = (option: IMultiSelectOption) =>
+ options.filter(child => child.parentId === option.id);
+ const getParent = (option: IMultiSelectOption) =>
+ options.find(parent => option.parentId === parent.id);
+ const foundFn = (item: any) => { filteredOpts.push(item); founded++; };
+ const notFoundFn = prevResults ? () => removedFromPrevResult++ : () => { };
+
+ for (; i < optsLength && founded < maxFound; ++i) {
+ const option = options[i];
+ const directMatch = doesOptionMatch(option);
+
+ if (directMatch) {
+ foundFn(option);
+ continue;
+ }
+
+ if (typeof option.parentId === 'undefined') {
+ const childrenMatch = getChildren(option).some(doesOptionMatch);
+
+ if (childrenMatch) {
+ foundFn(option);
+ continue;
+ }
+ }
+
+ if (typeof option.parentId !== 'undefined') {
+ const parentMatch = doesOptionMatch(getParent(option));
+
+ if (parentMatch) {
+ foundFn(option);
+ continue;
+ }
+ }
+
+ notFoundFn();
+ }
+
+ const totalIterations = i + prevResultShift;
+
+ this._searchCache[str] = filteredOpts;
+ this._searchCacheInclusive[str] = i === optsLength || totalIterations;
+ this._prevSkippedItems[str] = removedFromPrevResult + prevResultShift;
+
+ return filteredOpts;
+ }
+
+ private _limitRenderedItems(items: T[], limit: number): T[] {
+ return items.length > limit && limit > 0 ? items.slice(0, limit) : items;
+ }
+}
diff --git a/frontend/src/app/components/ngx-bootstrap-multiselect/types.ts b/frontend/src/app/components/ngx-bootstrap-multiselect/types.ts
new file mode 100644
index 000000000..0b04ea8aa
--- /dev/null
+++ b/frontend/src/app/components/ngx-bootstrap-multiselect/types.ts
@@ -0,0 +1,82 @@
+export interface IMultiSelectOption {
+ id: any;
+ name: string;
+ disabled?: boolean;
+ isLabel?: boolean;
+ parentId?: any;
+ params?: any;
+ classes?: string;
+ image?: string;
+}
+
+export interface IMultiSelectSettings {
+ pullRight?: boolean;
+ enableSearch?: boolean;
+ closeOnClickOutside?: boolean;
+ /**
+ * 0 - By default
+ * If `enableSearch=true` and total amount of items more then `searchRenderLimit` (0 - No limit)
+ * then render items only when user typed more then or equal `searchRenderAfter` charachters
+ */
+ searchRenderLimit?: number;
+ /**
+ * 3 - By default
+ */
+ searchRenderAfter?: number;
+ /**
+ * 0 - By default
+ * If >0 will render only N first items
+ */
+ searchMaxLimit?: number;
+ /**
+ * 0 - By default
+ * Used with searchMaxLimit to further limit rendering for optimization
+ * Should be less than searchMaxLimit to take effect
+ */
+ searchMaxRenderedItems?: number;
+ checkedStyle?: 'checkboxes' | 'glyphicon' | 'fontawesome' | 'visual';
+ buttonClasses?: string;
+ itemClasses?: string;
+ containerClasses?: string;
+ selectionLimit?: number;
+ minSelectionLimit?: number;
+ closeOnSelect?: boolean;
+ autoUnselect?: boolean;
+ showCheckAll?: boolean;
+ showUncheckAll?: boolean;
+ fixedTitle?: boolean;
+ dynamicTitleMaxItems?: number;
+ maxHeight?: string;
+ displayAllSelectedText?: boolean;
+ isLazyLoad?: boolean;
+ loadViewDistance?: number;
+ stopScrollPropagation?: boolean;
+ selectAddedValues?: boolean;
+ /**
+ * false - By default
+ * If activated label IDs don't count and won't be written to the model.
+ */
+ ignoreLabels?: boolean;
+ /**
+ * false - By default
+ * If activated, the title will show selections in the order they were selected.
+ */
+ maintainSelectionOrderInTitle?: boolean;
+ /**
+ * @default true
+ * Set the focus back to the input control when the dropdown closed
+ */
+ focusBack?: boolean;
+}
+
+export interface IMultiSelectTexts {
+ checkAll?: string;
+ uncheckAll?: string;
+ checked?: string;
+ checkedPlural?: string;
+ searchPlaceholder?: string;
+ searchEmptyResult?: string;
+ searchNoRenderText?: string;
+ defaultTitle?: string;
+ allSelected?: string;
+}
diff --git a/production/install b/production/install
index 19ff128a6..fc89e7e30 100755
--- a/production/install
+++ b/production/install
@@ -1295,18 +1295,6 @@ if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then
echo "[*] Installing Bitcoin Mainnet electrs start script"
osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-mainnet" "${BITCOIN_ELECTRS_HOME}"
- echo "[*] Installing Bitcoin crontab"
- case $OS in
- FreeBSD)
- echo [*] FIXME: must only crontab enabled daemons
- osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/bitcoin.crontab"
- osSudo "${ROOT_USER}" crontab -u "${MINFEE_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/minfee.crontab"
- ;;
- Debian)
- (crontab -l ; echo "@reboot sleep 30 ; screen -dmS mainnet /bitcoin/electrs/electrs-start-mainnet") | osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" -
- ;;
- esac
-
echo "[*] Configuring Bitcoin Mainnet RPC credentials in electrs start script"
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-mainnet"
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-mainnet"
@@ -1321,13 +1309,6 @@ if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then
echo "[*] Installing Bitcoin Testnet electrs start script"
osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-testnet" "${BITCOIN_ELECTRS_HOME}"
- case $OS in
- Debian)
- echo "[*] Installing Bitcoin-testnet crontab"
- (crontab -l ; echo "@reboot sleep 70 ; screen -dmS testnet /bitcoin/electrs/electrs-start-testnet") | osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" -
- ;;
- esac
-
echo "[*] Configuring Bitcoin Testnet RPC credentials in electrs start script"
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-testnet"
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-testnet"
@@ -1342,13 +1323,6 @@ if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then
echo "[*] Installing Bitcoin Signet electrs start script"
osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-signet" "${BITCOIN_ELECTRS_HOME}"
- case $OS in
- Debian)
- echo "[*] Installing Bitcoin-signet crontab"
- (crontab -l ; echo "@reboot sleep 90 ; screen -dmS signet /bitcoin/electrs/electrs-start-signet") | osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" -
- ;;
- esac
-
echo "[*] Configuring Bitcoin Signet RPC credentials in electrs start script"
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-signet"
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-signet"
@@ -1369,9 +1343,6 @@ if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then
echo [*] FIXME: must only crontab enabled daemons
osSudo "${ROOT_USER}" crontab -u "${ELEMENTS_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/elements.crontab"
;;
- Debian)
- (crontab -l ; echo "6 * * * * cd $HOME/asset_registry_db && git pull origin master >/dev/null 2>&1") | osSudo "${ROOT_USER}" crontab -u "${ELEMENTS_USER}" -
- ;;
esac
echo "[*] Configuring Elements Liquid RPC credentials in electrs start script"
@@ -1388,13 +1359,6 @@ if [ "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then
echo "[*] Installing Elements Liquid Testnet electrs start script"
osSudo "${ROOT_USER}" install -c -o "${ELEMENTS_USER}" -g "${ELEMENTS_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-liquidtestnet" "${ELEMENTS_ELECTRS_HOME}"
- case $OS in
- Debian)
- echo "[*] Installing Elements-testnet crontab"
- (crontab -l ; echo "6 * * * * cd $HOME/asset_registry_testnet_db && git pull origin master >/dev/null 2>&1") | osSudo "${ROOT_USER}" crontab -u "${ELEMENTS_USER}" -
- ;;
- esac
-
echo "[*] Installing Elements Liquid Testnet RPC credentials"
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${ELEMENTS_HOME}/elements.conf"
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${ELEMENTS_HOME}/elements.conf"
@@ -1407,6 +1371,45 @@ if [ "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then
osSudo "${ROOT_USER}" sed -i.orig "s!__ELECTRS_DATA_ROOT__!${ELECTRS_DATA_ROOT}!" "${ELEMENTS_ELECTRS_HOME}/electrs-start-liquidtestnet"
fi
+################################
+# Install all Electrs Cronjobs #
+################################
+echo "[*] Installing crontabs"
+case $OS in
+ FreeBSD)
+ echo [*] FIXME: must only crontab enabled daemons
+ osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/bitcoin.crontab"
+ osSudo "${ROOT_USER}" crontab -u "${MINFEE_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/minfee.crontab"
+ ;;
+ Debian)
+ crontab_bitcoin=()
+ if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then
+ echo [*] Installing Electrs Mainnet Cronjob
+ crontab_bitcoin+="@reboot sleep 30 ; screen -dmS mainnet /bitcoin/electrs/electrs-start-mainnet\n"
+ fi
+ if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then
+ echo [*] Installing Electrs Testnet Cronjob
+ crontab_bitcoin+="@reboot sleep 70 ; screen -dmS testnet /bitcoin/electrs/electrs-start-testnet\n"
+ fi
+ if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then
+ echo [*] Installing Electrs Signet Cronjob
+ crontab_bitcoin+="@reboot sleep 90 ; screen -dmS signet /bitcoin/electrs/electrs-start-signet\n"
+ fi
+ echo "${crontab_bitcoin}" | crontab -u "${BITCOIN_USER}" -
+
+ crontab_elements=()
+ if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then
+ echo [*] Installing Liquid Asset Mainnet Cronjob
+ crontab_elements+="6 * * * * cd $HOME/asset_registry_db && git pull origin master >/dev/null 2>&1\n"
+ fi
+ if [ "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then
+ echo [*] Installing Liquid Asset Testnet Cronjob
+ crontab_elements+="6 * * * * cd $HOME/asset_registry_testnet_db && git pull origin master >/dev/null 2>&1\n"
+ fi
+ echo "${crontab_elements}" | crontab -u "${ELEMENTS_USER}" -
+ ;;
+esac
+
#####################################
# Bisq instance for Bitcoin Mainnet #
#####################################
@@ -1553,6 +1556,29 @@ case $OS in
;;
esac
+##### OS set Linux user ulimits
+
+echo "[*] Setting ulimits for users"
+case $OS in
+
+ FreeBSD)
+ ;;
+
+ Debian)
+ cat >> /etc/security/limits.conf <> /etc/pam.d/common-session
+ ;;
+esac
+
+
+
+
+
##### OS services
#if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then
@@ -1628,6 +1654,8 @@ esac
##### finish
+echo 'Please reboot to start all the services.'
+
echo '[*] Done!'
exit 0
diff --git a/production/mempool-build-all b/production/mempool-build-all
index 11342605d..5ac25f7e4 100755
--- a/production/mempool-build-all
+++ b/production/mempool-build-all
@@ -56,7 +56,7 @@ build_frontend()
if [ ! -e "mempool-frontend-config.json" ];then
cp "${HOME}/mempool/production/mempool-frontend-config.${site}.json" "mempool-frontend-config.json"
fi
- npm install --prod --no-optional || exit 1
+ npm install --omit=dev --omit=optional || exit 1
npm run build || exit 1
}
@@ -75,7 +75,7 @@ build_backend()
-e "s!__ELEMENTS_RPC_PASS__!${ELEMENTS_RPC_PASS}!" \
"mempool-config.json"
fi
- npm install --prod --no-optional || exit 1
+ npm install --omit=dev --omit=optional || exit 1
npm run build || exit 1
}