@datagrok/bio 2.11.28 → 2.11.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -1
- package/dist/23.js +2 -0
- package/dist/23.js.map +1 -0
- package/dist/282.js +2 -0
- package/dist/282.js.map +1 -0
- package/dist/36.js +2 -0
- package/dist/36.js.map +1 -0
- package/dist/40.js +2 -0
- package/dist/40.js.map +1 -0
- package/dist/413.js +2 -0
- package/dist/413.js.map +1 -0
- package/dist/42.js +1 -1
- package/dist/42.js.map +1 -1
- package/dist/427.js +2 -0
- package/dist/427.js.map +1 -0
- package/dist/65.js +2 -0
- package/dist/65.js.map +1 -0
- package/dist/709.js +3 -0
- package/dist/709.js.map +1 -0
- package/dist/package-test.js +1 -1
- package/dist/package-test.js.map +1 -1
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/files/tests/to-atomic-level-msa-fasta-input.csv +10 -0
- package/files/tests/to-atomic-level-msa-fasta-output.csv +2178 -0
- package/package.json +9 -9
- package/src/package.ts +12 -10
- package/src/tests/msa-tests.ts +2 -2
- package/src/tests/pepsea-tests.ts +1 -1
- package/src/tests/renderers-test.ts +16 -16
- package/src/tests/splitters-test.ts +7 -12
- package/src/tests/substructure-filters-tests.ts +277 -55
- package/src/tests/to-atomic-level-tests.ts +32 -22
- package/src/tests/utils/sequences-generators.ts +6 -3
- package/src/tests/utils.ts +6 -6
- package/src/utils/cell-renderer.ts +2 -0
- package/src/utils/docker.ts +36 -0
- package/src/viewers/vd-regions-viewer.ts +4 -0
- package/src/viewers/web-logo-viewer.ts +4 -0
- package/src/widgets/bio-substructure-filter-helm.ts +168 -0
- package/src/widgets/bio-substructure-filter-types.ts +131 -0
- package/src/widgets/bio-substructure-filter.ts +215 -175
- package/src/widgets/composition-analysis-widget.ts +1 -1
- package/dist/100.js +0 -2
- package/dist/100.js.map +0 -1
- package/dist/118.js +0 -2
- package/dist/118.js.map +0 -1
- package/dist/361.js +0 -2
- package/dist/361.js.map +0 -1
- package/dist/471.js +0 -2
- package/dist/471.js.map +0 -1
- package/dist/649.js +0 -2
- package/dist/649.js.map +0 -1
- package/dist/664.js +0 -2
- package/dist/664.js.map +0 -1
- package/dist/886.js +0 -3
- package/dist/886.js.map +0 -1
- /package/dist/{886.js.LICENSE.txt → 709.js.LICENSE.txt} +0 -0
|
@@ -21,22 +21,30 @@ import {_package} from '../package-test';
|
|
|
21
21
|
const appPath = 'System:AppData/Bio';
|
|
22
22
|
const fileSource = new DG.FileSource(appPath);
|
|
23
23
|
|
|
24
|
-
const
|
|
25
|
-
PT
|
|
26
|
-
DNA
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const inputPath: { [k: string]: string } = {
|
|
31
|
-
PT: 'tests/to-atomic-level-peptides-fasta-input.csv',
|
|
32
|
-
DNA: 'tests/to-atomic-level-dna-fasta-input.csv',
|
|
33
|
-
MSA: 'tests/to-atomic-level-msa-separator-input.csv',
|
|
34
|
-
};
|
|
24
|
+
const enum Tests {
|
|
25
|
+
PT = 'peptides-fasta',
|
|
26
|
+
DNA = 'dna-fasta',
|
|
27
|
+
MSA_SEPARATOR = 'msa-separator',
|
|
28
|
+
MSA_FASTA = 'msa-fasta',
|
|
29
|
+
}
|
|
35
30
|
|
|
36
|
-
const
|
|
37
|
-
PT:
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
const TestsData: { [testName: string]: { inPath: string, outPath: string } } = {
|
|
32
|
+
[Tests.PT]: {
|
|
33
|
+
inPath: 'tests/to-atomic-level-peptides-fasta-input.csv',
|
|
34
|
+
outPath: 'tests/to-atomic-level-peptides-fasta-output.csv'
|
|
35
|
+
},
|
|
36
|
+
[Tests.DNA]: {
|
|
37
|
+
inPath: 'tests/to-atomic-level-dna-fasta-input.csv',
|
|
38
|
+
outPath: 'tests/to-atomic-level-dna-fasta-output.csv'
|
|
39
|
+
},
|
|
40
|
+
[Tests.MSA_SEPARATOR]: {
|
|
41
|
+
inPath: 'tests/to-atomic-level-msa-separator-input.csv',
|
|
42
|
+
outPath: 'tests/to-atomic-level-msa-separator-output.csv'
|
|
43
|
+
},
|
|
44
|
+
[Tests.MSA_FASTA]: {
|
|
45
|
+
inPath: 'tests/to-atomic-level-msa-fasta-input.csv',
|
|
46
|
+
outPath: 'tests/to-atomic-level-msa-fasta-output.csv'
|
|
47
|
+
},
|
|
40
48
|
};
|
|
41
49
|
|
|
42
50
|
const inputColName = 'sequence';
|
|
@@ -57,10 +65,12 @@ category('toAtomicLevel', async () => {
|
|
|
57
65
|
await setUserLibSettingsForTests();
|
|
58
66
|
await monomerLibHelper.loadLibraries(true);
|
|
59
67
|
|
|
60
|
-
for (const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
for (const [testName, testData] of Object.entries(TestsData)) {
|
|
69
|
+
const inputPath = testData.inPath;
|
|
70
|
+
|
|
71
|
+
sourceDf[testName] = DG.DataFrame.fromCsv((await fileSource.readAsText(testData.inPath)).replace(/\n$/, ''));
|
|
72
|
+
await grok.data.detectSemanticTypes(sourceDf[testName]);
|
|
73
|
+
targetDf[testName] = DG.DataFrame.fromCsv((await fileSource.readAsText(testData.outPath)).replace(/\n$/, ''));
|
|
64
74
|
}
|
|
65
75
|
});
|
|
66
76
|
|
|
@@ -79,9 +89,9 @@ category('toAtomicLevel', async () => {
|
|
|
79
89
|
expectArray(obtainedArray, expectedArray);
|
|
80
90
|
}
|
|
81
91
|
|
|
82
|
-
for (const
|
|
83
|
-
test(`${
|
|
84
|
-
await getTestResult(sourceDf[
|
|
92
|
+
for (const [testName, testData] of Object.entries(TestsData)) {
|
|
93
|
+
test(`${testName}`, async () => {
|
|
94
|
+
await getTestResult(sourceDf[testName], targetDf[testName]);
|
|
85
95
|
});
|
|
86
96
|
}
|
|
87
97
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import * as grok from 'datagrok-api/grok';
|
|
2
2
|
import * as ui from 'datagrok-api/ui';
|
|
3
3
|
import * as DG from 'datagrok-api/dg';
|
|
4
|
+
|
|
4
5
|
import {ALIGNMENT, ALPHABET, NOTATION, TAGS as bioTAGS} from '@datagrok-libraries/bio/src/utils/macromolecule';
|
|
5
|
-
import {
|
|
6
|
+
import {expect} from '@datagrok-libraries/utils/src/test';
|
|
7
|
+
|
|
8
|
+
import {awaitGrid} from '../utils';
|
|
6
9
|
|
|
7
10
|
|
|
8
11
|
export function generateManySequences(): DG.Column[] {
|
|
@@ -32,8 +35,8 @@ export async function performanceTest(generateFunc: () => DG.Column[], testName:
|
|
|
32
35
|
const col: DG.Column = df.columns.byName('MSA');
|
|
33
36
|
const view: DG.TableView = grok.shell.addTableView(df);
|
|
34
37
|
|
|
35
|
-
await
|
|
36
|
-
|
|
38
|
+
await awaitGrid(view.grid);
|
|
39
|
+
expect(view.grid.dataFrame.id, df.id);
|
|
37
40
|
|
|
38
41
|
const endTime: number = Date.now();
|
|
39
42
|
const elapsedTime: number = endTime - startTime;
|
package/src/tests/utils.ts
CHANGED
|
@@ -3,6 +3,8 @@ import * as grok from 'datagrok-api/grok';
|
|
|
3
3
|
|
|
4
4
|
import {delay, expect, testEvent} from '@datagrok-libraries/utils/src/test';
|
|
5
5
|
|
|
6
|
+
import {startDockerContainer} from '../utils/docker';
|
|
7
|
+
|
|
6
8
|
import {_package} from '../package-test';
|
|
7
9
|
|
|
8
10
|
export async function loadFileAsText(name: string): Promise<string> {
|
|
@@ -34,12 +36,10 @@ export function _testTableIsNotEmpty(table: DG.DataFrame): void {
|
|
|
34
36
|
|
|
35
37
|
/** Waits if container is not started
|
|
36
38
|
* @param {number} ms - time to wait in milliseconds */
|
|
37
|
-
export async function awaitContainerStart(ms: number =
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// TODO: Enable with new JS API version
|
|
42
|
-
// await grok.dapi.docker.dockerContainers.run(container.id, true);
|
|
39
|
+
export async function awaitContainerStart(ms: number = 30000): Promise<void> {
|
|
40
|
+
const dc = await grok.dapi.docker.dockerContainers.filter('bio').first();
|
|
41
|
+
const dcId = dc.id;
|
|
42
|
+
await startDockerContainer(dcId, ms);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export async function awaitGrid(grid: DG.Grid, timeout: number = 5000): Promise<void> {
|
|
@@ -215,6 +215,8 @@ export class MacromoleculeSequenceCellRenderer extends DG.GridCellRenderer {
|
|
|
215
215
|
|
|
216
216
|
const tempReferenceSequence: string | null = tableColTemp[tempTAGS.referenceSequence];
|
|
217
217
|
const tempCurrentWord: string | null = tableColTemp[tempTAGS.currentWord];
|
|
218
|
+
if (tempCurrentWord && tableCol?.dataFrame?.currentRowIdx === -1)
|
|
219
|
+
tableColTemp[tempTAGS.currentWord] = null;
|
|
218
220
|
const referenceSequence: ISeqSplitted = splitterFunc(
|
|
219
221
|
((tempReferenceSequence != null) && (tempReferenceSequence != '')) ?
|
|
220
222
|
tempReferenceSequence : tempCurrentWord ?? '');
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as grok from 'datagrok-api/grok';
|
|
2
|
+
import * as ui from 'datagrok-api/ui';
|
|
3
|
+
import * as DG from 'datagrok-api/dg';
|
|
4
|
+
import {delay} from '@datagrok-libraries/utils/src/test';
|
|
5
|
+
|
|
6
|
+
export async function startDockerContainer(dcId: string, timeout: number = 30000): Promise<void> {
|
|
7
|
+
// TODO: Use the new dockerContainers API
|
|
8
|
+
const res = await grok.dapi.docker.dockerContainers.run(dcId /*, true */);
|
|
9
|
+
let end: boolean = false;
|
|
10
|
+
for (let i = 0; i < timeout / 200; ++i) {
|
|
11
|
+
const dc = await grok.dapi.docker.dockerContainers.find(dcId);
|
|
12
|
+
switch (dc.status) {
|
|
13
|
+
case 'stopped': {
|
|
14
|
+
await grok.dapi.docker.dockerContainers.run(dcId);
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
case 'pending change':
|
|
18
|
+
case 'changing': {
|
|
19
|
+
// skip to wait
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
case 'checking':
|
|
23
|
+
case 'started': {
|
|
24
|
+
end = true;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
case 'error': {
|
|
28
|
+
throw new Error('Docker container error state.');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (end) break;
|
|
32
|
+
await delay(200);
|
|
33
|
+
}
|
|
34
|
+
if (!end) throw new Error('Docker container run timeout.');
|
|
35
|
+
// this.dc = await grok.dapi.docker.dockerContainers.find(dcId);
|
|
36
|
+
}
|
|
@@ -520,5 +520,9 @@ export class VdRegionsViewer extends DG.JsViewer implements IVdRegionsViewer {
|
|
|
520
520
|
await testEvent(this.onRendered, () => {}, () => {
|
|
521
521
|
this.invalidate();
|
|
522
522
|
}, timeout);
|
|
523
|
+
|
|
524
|
+
// Rethrow stored syncer error (for test purposes)
|
|
525
|
+
const viewErrors = this.viewSyncer.resetErrors();
|
|
526
|
+
if (viewErrors.length > 0) throw viewErrors[0];
|
|
523
527
|
}
|
|
524
528
|
}
|
|
@@ -1297,6 +1297,10 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
|
|
|
1297
1297
|
await testEvent(this.onRendered, () => {}, () => {
|
|
1298
1298
|
this.invalidate();
|
|
1299
1299
|
}, timeout);
|
|
1300
|
+
|
|
1301
|
+
// Rethrow stored syncer error (for test purposes)
|
|
1302
|
+
const viewErrors = this.viewSyncer.resetErrors();
|
|
1303
|
+
if (viewErrors.length > 0) throw viewErrors[0];
|
|
1300
1304
|
}
|
|
1301
1305
|
}
|
|
1302
1306
|
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import * as grok from 'datagrok-api/grok';
|
|
2
|
+
import * as ui from 'datagrok-api/ui';
|
|
3
|
+
import * as DG from 'datagrok-api/dg';
|
|
4
|
+
|
|
5
|
+
import $ from 'cash-dom';
|
|
6
|
+
import {fromEvent, Observable, Subject, Unsubscribable} from 'rxjs';
|
|
7
|
+
|
|
8
|
+
import {IHelmWebEditor, IWebEditorApp} from '@datagrok-libraries/bio/src/helm/types';
|
|
9
|
+
import {getHelmHelper} from '@datagrok-libraries/bio/src/helm/helm-helper';
|
|
10
|
+
import {ILogger} from '@datagrok-libraries/bio/src/utils/logger';
|
|
11
|
+
import {errInfo} from '@datagrok-libraries/bio/src/utils/err-info';
|
|
12
|
+
import {delay, testEvent} from '@datagrok-libraries/utils/src/test';
|
|
13
|
+
|
|
14
|
+
import {updateDivInnerHTML} from '../utils/ui-utils';
|
|
15
|
+
import {helmSubstructureSearch} from '../substructure-search/substructure-search';
|
|
16
|
+
import {BioFilterBase, BioFilterProps} from './bio-substructure-filter-types';
|
|
17
|
+
|
|
18
|
+
import {_package} from '../package';
|
|
19
|
+
|
|
20
|
+
export class HelmBioFilter extends BioFilterBase<BioFilterProps> /* implements IRenderer */ {
|
|
21
|
+
readonly emptyProps = new BioFilterProps('');
|
|
22
|
+
|
|
23
|
+
helmEditor: IHelmWebEditor;
|
|
24
|
+
_filterPanel = ui.div('', {style: {cursor: 'pointer'}});
|
|
25
|
+
|
|
26
|
+
private readonly logger: ILogger;
|
|
27
|
+
|
|
28
|
+
private static viewerCounter: number = -1;
|
|
29
|
+
private readonly viewerId: number = ++HelmBioFilter.viewerCounter;
|
|
30
|
+
|
|
31
|
+
private viewerToLog(): string { return `HelmBioFilter<${this.viewerId}>`; }
|
|
32
|
+
|
|
33
|
+
get type(): string { return 'HelmBioFilter'; }
|
|
34
|
+
|
|
35
|
+
constructor() {
|
|
36
|
+
super();
|
|
37
|
+
this.logger = _package.logger;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
viewSubs: Unsubscribable[] = [];
|
|
41
|
+
|
|
42
|
+
async detach(): Promise<void> {
|
|
43
|
+
await super.detach();
|
|
44
|
+
for (const sub of this.viewSubs) sub.unsubscribe();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async attach(): Promise<void> {
|
|
48
|
+
const logPrefix = `${this.viewerToLog()}.init()`;
|
|
49
|
+
try {
|
|
50
|
+
const helmHelper = await getHelmHelper();
|
|
51
|
+
this.helmEditor = helmHelper.createHelmWebEditor();
|
|
52
|
+
this.logger.warning('TEST: HelmBioFilter.init().sync() waitForElementInDom waiting...');
|
|
53
|
+
await ui.tools.waitForElementInDom(this._filterPanel);
|
|
54
|
+
this.logger.warning('TEST: HelmBioFilter.init().sync() waitForElementInDom ready');
|
|
55
|
+
this.updateFilterPanel();
|
|
56
|
+
let webEditorHost: HTMLDivElement | null;
|
|
57
|
+
let webEditorApp: IWebEditorApp | null;
|
|
58
|
+
// TODO: Unsubscribe 'click' and 'sizeChanged'
|
|
59
|
+
this.viewSubs.push(fromEvent(this._filterPanel, 'click').subscribe(() => {
|
|
60
|
+
webEditorHost = ui.div();
|
|
61
|
+
webEditorApp = helmHelper.createWebEditorApp(webEditorHost, this.props.substructure);
|
|
62
|
+
const dlg = ui.dialog({showHeader: false, showFooter: true})
|
|
63
|
+
.add(webEditorHost!)
|
|
64
|
+
.onOK(() => {
|
|
65
|
+
try {
|
|
66
|
+
const webEditorValue = webEditorApp!.canvas.getHelm(true)
|
|
67
|
+
.replace(/<\/span>/g, '').replace(/<span style='background:#bbf;'>/g, '');
|
|
68
|
+
this.props = new BioFilterProps(webEditorValue);
|
|
69
|
+
} catch (err: any) {
|
|
70
|
+
this.logger.error(err);
|
|
71
|
+
} finally {
|
|
72
|
+
$(webEditorHost).empty();
|
|
73
|
+
webEditorHost = null;
|
|
74
|
+
webEditorApp = null;
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
.onCancel(() => {
|
|
78
|
+
$(webEditorHost).empty();
|
|
79
|
+
webEditorHost = null;
|
|
80
|
+
webEditorApp = null;
|
|
81
|
+
})
|
|
82
|
+
.show({modal: true, fullScreen: true});
|
|
83
|
+
// const onCloseSub = dlg.onClose.subscribe(() => {
|
|
84
|
+
// onCloseSub.unsubscribe();
|
|
85
|
+
// $(editorDiv).empty();
|
|
86
|
+
// editorDiv = undefined;
|
|
87
|
+
// webEditor = undefined;
|
|
88
|
+
// });
|
|
89
|
+
}));
|
|
90
|
+
this.viewSubs.push(ui.onSizeChanged(this._filterPanel).subscribe((_) => {
|
|
91
|
+
try {
|
|
92
|
+
if (!!webEditorApp) {
|
|
93
|
+
const helmString = webEditorApp.canvas.getHelm(true)
|
|
94
|
+
.replace(/<\/span>/g, '').replace(/<span style='background:#bbf;'>/g, '');
|
|
95
|
+
this.updateFilterPanel(helmString);
|
|
96
|
+
}
|
|
97
|
+
} catch (err: any) {
|
|
98
|
+
const [errMsg, errStack] = errInfo(err);
|
|
99
|
+
this.logger.error(errMsg, undefined, errStack);
|
|
100
|
+
}
|
|
101
|
+
}));
|
|
102
|
+
} catch (err: any) {
|
|
103
|
+
const [errMsg, _errStack] = errInfo(err);
|
|
104
|
+
const fp = this._filterPanel;
|
|
105
|
+
fp.innerText = 'error';
|
|
106
|
+
fp.classList.add('d4-error');
|
|
107
|
+
ui.tooltip.bind(fp, errMsg);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
applyProps() {
|
|
112
|
+
if (!this.helmEditor) return; // helmEditor is not created, the filter is not in dom yet
|
|
113
|
+
this.helmEditor.editor.setHelm(this.props.substructure);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
get filterPanel() {
|
|
117
|
+
return this._filterPanel;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
updateFilterPanel(helmString?: string) {
|
|
121
|
+
if (!this.helmEditor) throw new Error('helmEditor is not created, the filter is not in dom yet');
|
|
122
|
+
|
|
123
|
+
const width = this._filterPanel.parentElement!.clientWidth < 100 ? 100 :
|
|
124
|
+
this._filterPanel.parentElement!.clientWidth;
|
|
125
|
+
const height = width / 2;
|
|
126
|
+
if (!helmString) {
|
|
127
|
+
const editDiv = ui.divText('Click to edit', 'helm-substructure-filter');
|
|
128
|
+
updateDivInnerHTML(this._filterPanel, editDiv);
|
|
129
|
+
} else {
|
|
130
|
+
updateDivInnerHTML(this._filterPanel, this.helmEditor.host);
|
|
131
|
+
this.helmEditor.editor.setHelm(helmString);
|
|
132
|
+
this.helmEditor.resizeEditor(width, height);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async substructureSearch(column: DG.Column): Promise<DG.BitSet | null> {
|
|
137
|
+
await delay(10);
|
|
138
|
+
const res = await helmSubstructureSearch(this.props.substructure, column);
|
|
139
|
+
return res;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// // -- IRenderer --
|
|
143
|
+
//
|
|
144
|
+
// private _onRendered = new Subject<void>();
|
|
145
|
+
//
|
|
146
|
+
// get onRendered(): Observable<void> { return this._onRendered; }
|
|
147
|
+
//
|
|
148
|
+
// invalidate(caller?: string): void {
|
|
149
|
+
// const logPrefix = `${this.viewerToLog()}.invalidate(${caller ? ` <- ${caller} ` : ''})`;
|
|
150
|
+
// this.viewSyncer.sync(logPrefix, async () => { this._onRendered.next(); });
|
|
151
|
+
// }
|
|
152
|
+
//
|
|
153
|
+
// async awaitRendered(timeout: number = 10000): Promise<void> {
|
|
154
|
+
// const callLog = `awaitRendered( ${timeout} )`;
|
|
155
|
+
// const logPrefix = `${this.viewerToLog()}.${callLog}`;
|
|
156
|
+
// await delay(0);
|
|
157
|
+
// await testEvent(this.onRendered, () => {
|
|
158
|
+
// this.logger.debug(`${logPrefix}, ` + '_onRendered event caught');
|
|
159
|
+
// }, () => {
|
|
160
|
+
// this.invalidate(callLog);
|
|
161
|
+
// }, timeout, `${logPrefix} ${timeout} timeout`);
|
|
162
|
+
//
|
|
163
|
+
// // Rethrow stored syncer error (for test purposes)
|
|
164
|
+
// const viewErrors = this.viewSyncer.resetErrors();
|
|
165
|
+
// if (viewErrors.length > 0) throw viewErrors[0];
|
|
166
|
+
// }
|
|
167
|
+
}
|
|
168
|
+
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import * as ui from 'datagrok-api/ui';
|
|
2
|
+
import * as DG from 'datagrok-api/dg';
|
|
3
|
+
import * as grok from 'datagrok-api/grok';
|
|
4
|
+
|
|
5
|
+
import {Observable, Subject, Unsubscribable} from 'rxjs';
|
|
6
|
+
import {_package} from '../package';
|
|
7
|
+
|
|
8
|
+
export interface IFilterProps {
|
|
9
|
+
get onChanged(): Observable<void>;
|
|
10
|
+
|
|
11
|
+
save(): object;
|
|
12
|
+
apply(propsObj: object): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Fasta and Helm */
|
|
16
|
+
export class BioFilterProps implements IFilterProps {
|
|
17
|
+
private _onChanged: Subject<void> = new Subject<void>();
|
|
18
|
+
|
|
19
|
+
get onChanged(): Observable<void> { return this._onChanged; }
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
public substructure: string
|
|
23
|
+
) {
|
|
24
|
+
return new Proxy(this, {
|
|
25
|
+
set: (target: any, key: string | symbol, value: any) => {
|
|
26
|
+
_package.logger.debug(`BioFilterProps.set ${key.toString()}( '${value}' )`);
|
|
27
|
+
target[key] = value;
|
|
28
|
+
this._onChanged.next();
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
save(): object {
|
|
35
|
+
const propsObj = {};
|
|
36
|
+
for (const [key, value] of Object.entries(this)) {
|
|
37
|
+
if (key !== '_onChanged') {
|
|
38
|
+
// @ts-ignore
|
|
39
|
+
propsObj[key] = this[key];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return propsObj;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
apply(propsObj: object) {
|
|
46
|
+
for (const [key, value] of Object.entries(this)) {
|
|
47
|
+
if (key !== '_onChanged') {
|
|
48
|
+
// @ts-ignore
|
|
49
|
+
this[key] = propsObj[key];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface IBioFilter {
|
|
56
|
+
get type(): string;
|
|
57
|
+
|
|
58
|
+
get props(): IFilterProps;
|
|
59
|
+
set props(value: IFilterProps);
|
|
60
|
+
|
|
61
|
+
get onChanged(): Observable<void>;
|
|
62
|
+
get filterPanel(): HTMLElement;
|
|
63
|
+
get filterSummary(): string;
|
|
64
|
+
get isFiltering(): boolean;
|
|
65
|
+
|
|
66
|
+
attach(): Promise<void>;
|
|
67
|
+
detach(): Promise<void>;
|
|
68
|
+
resetFilter(): void;
|
|
69
|
+
substructureSearch(col: DG.Column): Promise<DG.BitSet | null>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Encapsulates input controls handling */
|
|
73
|
+
export abstract class BioFilterBase<TProps extends BioFilterProps> implements IBioFilter {
|
|
74
|
+
abstract get filterPanel(): HTMLElement;
|
|
75
|
+
|
|
76
|
+
abstract get emptyProps(): TProps;
|
|
77
|
+
|
|
78
|
+
onChanged: Subject<void> = new Subject<void>();
|
|
79
|
+
|
|
80
|
+
private _props: TProps;
|
|
81
|
+
protected _propsChanging: boolean = false;
|
|
82
|
+
private _propsOnChangedSub: Unsubscribable | null = null;
|
|
83
|
+
|
|
84
|
+
abstract get type(): string;
|
|
85
|
+
|
|
86
|
+
get props(): TProps {
|
|
87
|
+
if (!this._props) this._props = this.emptyProps;
|
|
88
|
+
return this._props;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
set props(value: TProps) {
|
|
92
|
+
this._propsChanging = true;
|
|
93
|
+
try {
|
|
94
|
+
if (this._propsOnChangedSub) {
|
|
95
|
+
this._propsOnChangedSub.unsubscribe();
|
|
96
|
+
this._propsOnChangedSub = null;
|
|
97
|
+
}
|
|
98
|
+
this._props = value;
|
|
99
|
+
this.applyProps();
|
|
100
|
+
this.onChanged.next();
|
|
101
|
+
this._propsOnChangedSub = this._props.onChanged
|
|
102
|
+
.subscribe(() => {
|
|
103
|
+
this.onChanged.next();
|
|
104
|
+
});
|
|
105
|
+
} finally {
|
|
106
|
+
this._propsChanging = false;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
abstract attach(): Promise<void>;
|
|
111
|
+
|
|
112
|
+
async detach(): Promise<void> {
|
|
113
|
+
if (this._propsOnChangedSub) {
|
|
114
|
+
this._propsOnChangedSub.unsubscribe();
|
|
115
|
+
this._propsOnChangedSub = null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
abstract applyProps(): void;
|
|
120
|
+
|
|
121
|
+
get filterSummary(): string { return this.props.substructure; };
|
|
122
|
+
|
|
123
|
+
get isFiltering(): boolean { return this.props.substructure !== ''; }
|
|
124
|
+
|
|
125
|
+
abstract substructureSearch(_column: DG.Column): Promise<DG.BitSet | null>;
|
|
126
|
+
|
|
127
|
+
resetFilter(): void {
|
|
128
|
+
this.props = this.emptyProps;
|
|
129
|
+
this.onChanged.next();
|
|
130
|
+
}
|
|
131
|
+
}
|