diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ec4e74eb4..e046c2865 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -30,10 +30,10 @@ jobs: restore-keys: | ${{ runner.os }}-node- - - name: Use Node.js 12 - uses: actions/setup-node@v1 + - name: Use Node.js 18 + uses: actions/setup-node@v3 with: - node-version: 12 + node-version: 18 - name: Build code run: npm run build diff --git a/.gitignore b/.gitignore index 880596013..c036fb85f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*~ .idea /web.config /node_modules/ diff --git a/Dockerfile b/Dockerfile index cea586ac5..8179d6f65 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ # Build the source -FROM node:14-alpine as builder +FROM docker.io/library/node:18.14.1-alpine@sha256:045b1a1c90bdfd8fcaad0769922aa16c401e31867d8bf5833365b0874884bbae as builder WORKDIR /code # First install dependencies. This part will be cached as long as -# the package(-lock).json files remain identical. -COPY package*.json /code/ +# the package.json file remains identical. +COPY package.json /code/ RUN npm install # Build code @@ -26,7 +26,7 @@ RUN find . -type f "(" \ | xargs -0 -n 1 gzip -kf # Production Nginx image -FROM nginxinc/nginx-unprivileged:1.23.0-alpine +FROM docker.io/nginxinc/nginx-unprivileged:1.23.3-alpine@sha256:c748ba587e7436aaa8729b64d4e0412410a486f0c592f0eec100fb3804ff9afd LABEL org.opencontainers.image.title="OHDSI-Atlas" LABEL org.opencontainers.image.authors="Joris Borgdorff , Lee Evans - www.ltscomputingllc.com" diff --git a/index.html b/index.html index 63e00ba73..1938905f2 100644 --- a/index.html +++ b/index.html @@ -17,7 +17,7 @@ - + + + - \ No newline at end of file + + + +
+ +
\ No newline at end of file diff --git a/js/components/conceptAddBox/concept-add-box.js b/js/components/conceptAddBox/concept-add-box.js index dd2e50213..d0ea0fab3 100644 --- a/js/components/conceptAddBox/concept-add-box.js +++ b/js/components/conceptAddBox/concept-add-box.js @@ -1,6 +1,7 @@ define([ 'knockout', 'components/conceptset/ConceptSetStore', + 'components/conceptset/InputTypes/ConceptSetItem', 'components/conceptset/utils', 'components/Component', 'utils/CommonUtils', @@ -10,18 +11,23 @@ define([ 'const', 'text!./concept-add-box.html', 'less!./concept-add-box.less', - 'databindings/cohortbuilder/dropupBinding', + 'databindings/cohortbuilder/dropupBinding', + './preview/conceptset-expression-preview', + './preview/included-preview', + './preview/included-preview-badge', + './preview/included-sourcecodes-preview', ], ( - ko, + ko, ConceptSetStore, + ConceptSetItem, conceptSetUtils, - Component, + Component, CommonUtils, AuthAPI, sharedState, config, - globalConstants, - view, + globalConstants, + view, ) => { const storeKeys = ConceptSetStore.sourceKeys(); @@ -35,6 +41,8 @@ define([ }); this.isActive = params.isActive || ko.observable(true); this.onSubmit = params.onSubmit; + this.noPreview = params.noPreview || false; + this.conceptsToAdd = params.concepts; this.canSelectSource = params.canSelectSource || false; this.isAdded = ko.observable(false); this.defaultSelectionOptions = { @@ -76,19 +84,95 @@ define([ this.messageTimeout = null; this.isDisabled = ko.pureComputed(() => !this.isActive() || !!this.isSuccessMessageVisible()); this.buttonTooltipText = conceptSetUtils.getPermissionsText(this.hasActiveConceptSets() || this.canCreateConceptSet(), 'create'); + + const tableOptions = CommonUtils.getTableOptions('L'); + this.previewConcepts = ko.observableArray(); + this.showPreviewModal = ko.observable(false); +/* this.showPreviewModal.subscribe((show) => { + if (!show) { + this.previewConcepts([]); + } + });*/ + this.previewTabsParams = ko.observable({ + tabs: [ + { + title: ko.i18n('components.conceptAddBox.previewModal.tabs.concepts', 'Concepts'), + key: 'expression', + componentName: 'conceptset-expression-preview', + componentParams: { + tableOptions, + conceptSetItems: this.previewConcepts + }, + }, + { + title: ko.i18n('cs.manager.tabs.includedConcepts', 'Included Concepts'), + key: 'included', + componentName: 'conceptset-list-included-preview', + componentParams: { + tableOptions, + previewConcepts: this.previewConcepts + }, + hasBadge: true, + }, + { + title: ko.i18n('cs.manager.tabs.includedSourceCodes', 'Source Codes'), + key: 'included-sourcecodes', + componentName: 'conceptset-list-included-sourcecodes-preview', + componentParams: { + tableOptions, + previewConcepts: this.previewConcepts + }, + } + ] + }); + } + + isPreviewAvailable() { + return !this.noPreview; } - + + handlePreview() { + const items = CommonUtils.buildConceptSetItems(this.conceptsToAdd(), this.selectionOptions()); + const itemsToAdd = items.map(item => new ConceptSetItem(item)); + const existingConceptsCopy = this.activeConceptSet() && this.activeConceptSet().current() + ? this.activeConceptSet().current().expression.items().map(item => new ConceptSetItem(ko.toJS(item))) + : []; + this.previewConcepts(itemsToAdd.concat(existingConceptsCopy)); + this.showPreviewModal(true); + } + handleSubmit() { clearTimeout(this.messageTimeout); this.isSuccessMessageVisible(true); - const conceptSet = this.canSelectSource && this.activeConceptSet() ? this.activeConceptSet() : undefined; - this.onSubmit(this.selectionOptions(), conceptSet); - this.selectionOptions(this.defaultSelectionOptions); this.messageTimeout = setTimeout(() => { this.isSuccessMessageVisible(false); }, 1000); + + if (this.noPreview) { + this.onSubmit(this.selectionOptions()); + return; + } + + const conceptSet = this.activeConceptSet() || ConceptSetStore.repository(); + + sharedState.activeConceptSet(conceptSet); + + // if concepts were previewed, then they already built and can have individual option flags! + if (this.previewConcepts().length > 0) { + if (!conceptSet.current()) { + conceptSetUtils.createRepositoryConceptSet(conceptSet); + } + conceptSet.current().expression.items(this.previewConcepts()); + + } else { + const items = CommonUtils.buildConceptSetItems(this.conceptsToAdd(), this.selectionOptions()); + conceptSetUtils.addItemsToConceptSet({items, conceptSetStore: conceptSet}); + } + + CommonUtils.clearConceptsSelectionState(this.conceptsToAdd()); + this.selectionOptions(this.defaultSelectionOptions); } - + toggleSelectionOption(option) { const options = this.selectionOptions(); this.selectionOptions({ diff --git a/js/components/conceptAddBox/concept-add-box.less b/js/components/conceptAddBox/concept-add-box.less index fb307e12d..2cb5bfd31 100644 --- a/js/components/conceptAddBox/concept-add-box.less +++ b/js/components/conceptAddBox/concept-add-box.less @@ -49,4 +49,8 @@ transition: visibility 0s 0.2s, opacity 0.2s linear; } } + + .preview-button { + margin-right: 10px; + } } \ No newline at end of file diff --git a/js/components/conceptAddBox/preview/conceptset-expression-preview.html b/js/components/conceptAddBox/preview/conceptset-expression-preview.html new file mode 100644 index 000000000..8ee85ef7f --- /dev/null +++ b/js/components/conceptAddBox/preview/conceptset-expression-preview.html @@ -0,0 +1,67 @@ +
+ + + + + + + + + + + + + +
+ + + + + + + + +
+ + +
\ No newline at end of file diff --git a/js/components/conceptAddBox/preview/conceptset-expression-preview.js b/js/components/conceptAddBox/preview/conceptset-expression-preview.js new file mode 100644 index 000000000..2d26f6ae2 --- /dev/null +++ b/js/components/conceptAddBox/preview/conceptset-expression-preview.js @@ -0,0 +1,108 @@ +define([ + 'knockout', + 'text!./conceptset-expression-preview.html', + 'components/Component', + 'utils/AutoBind', + 'utils/CommonUtils', + 'utils/Renderers', + 'components/conceptset/utils', + 'atlas-state', + 'components/conceptLegend/concept-legend', +], function ( + ko, + view, + Component, + AutoBind, + commonUtils, + renderers, + conceptSetUtils, + sharedState, +) { + class ConceptsetExpressionPreview extends AutoBind(Component) { + constructor(params) { + super(params); + this.conceptSetItems = params.conceptSetItems; + + this.commonUtils = commonUtils; + this.allExcludedChecked = ko.pureComputed(() => { + return this.conceptSetItems().find(item => !item.isExcluded()) === undefined; + }); + this.allDescendantsChecked = ko.pureComputed(() => { + return this.conceptSetItems().find(item => !item.includeDescendants()) === undefined; + }); + this.allMappedChecked = ko.pureComputed(() => { + return this.conceptSetItems().find(item => !item.includeMapped()) === undefined; + }); + + this.datatableLanguage = ko.i18n('datatable.language'); + + this.data = ko.pureComputed(() => this.conceptSetItems().map((item, idx) => ({ + ...item, + idx, + isSelected: ko.observable() + }))); + + this.tableOptions = params.tableOptions || commonUtils.getTableOptions('M'); + this.columns = [ + { + data: 'concept.CONCEPT_ID', + }, + { + data: 'concept.CONCEPT_CODE', + }, + { + render: commonUtils.renderBoundLink, + }, + { + data: 'concept.DOMAIN_ID', + }, + { + data: 'concept.STANDARD_CONCEPT', + visible: false, + }, + { + data: 'concept.STANDARD_CONCEPT_CAPTION', + }, + { + class: 'text-center', + orderable: false, + render: () => this.renderCheckbox('isExcluded'), + }, + { + class: 'text-center', + orderable: false, + render: () => this.renderCheckbox('includeDescendants'), + }, + { + class: 'text-center', + orderable: false, + render: () => this.renderCheckbox('includeMapped'), + }, + ]; + } + + renderCheckbox(field) { + return renderers.renderConceptSetCheckbox(ko.observable(true), field); + } + + toggleExcluded() { + this.selectAllConceptSetItems('isExcluded', this.allExcludedChecked()); + } + + toggleDescendants() { + this.selectAllConceptSetItems('includeDescendants', this.allDescendantsChecked()); + } + + toggleMapped() { + this.selectAllConceptSetItems('includeMapped', this.allMappedChecked()); + } + + async selectAllConceptSetItems(key, areAllSelected) { + this.conceptSetItems().forEach(conceptSetItem => { + conceptSetItem[key](!areAllSelected); + }) + } + } + + return commonUtils.build('conceptset-expression-preview', ConceptsetExpressionPreview, view); +}); \ No newline at end of file diff --git a/js/components/conceptAddBox/preview/included-preview-badge.html b/js/components/conceptAddBox/preview/included-preview-badge.html new file mode 100644 index 000000000..eacc6a23c --- /dev/null +++ b/js/components/conceptAddBox/preview/included-preview-badge.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/js/components/conceptAddBox/preview/included-preview-badge.js b/js/components/conceptAddBox/preview/included-preview-badge.js new file mode 100644 index 000000000..e770b709b --- /dev/null +++ b/js/components/conceptAddBox/preview/included-preview-badge.js @@ -0,0 +1,27 @@ +define([ + 'knockout', + 'text!./included-preview-badge.html', + 'components/Component', + 'utils/CommonUtils', + 'components/conceptsetInclusionCount/conceptsetInclusionCount', +], function( + ko, + view, + Component, + commonUtils, +){ + + class IncludedPreviewBadge extends Component { + + constructor(params){ + super(params); + this.expression = ko.pureComputed(() => { + return { + items: params.previewConcepts() + } + }); + } + } + + return commonUtils.build('conceptset-list-included-preview-badge', IncludedPreviewBadge, view); +}); \ No newline at end of file diff --git a/js/components/conceptAddBox/preview/included-preview.html b/js/components/conceptAddBox/preview/included-preview.html new file mode 100644 index 000000000..022cadf9d --- /dev/null +++ b/js/components/conceptAddBox/preview/included-preview.html @@ -0,0 +1,14 @@ + +
+ +
diff --git a/js/components/conceptAddBox/preview/included-preview.js b/js/components/conceptAddBox/preview/included-preview.js new file mode 100644 index 000000000..c140d521e --- /dev/null +++ b/js/components/conceptAddBox/preview/included-preview.js @@ -0,0 +1,85 @@ +define([ + 'knockout', + 'text!./included-preview.html', + 'components/Component', + 'utils/AutoBind', + 'utils/CommonUtils', + 'atlas-state', + 'services/Vocabulary', + 'components/conceptset/utils', +], function ( + ko, + view, + Component, + AutoBind, + commonUtils, + sharedState, + vocabularyService, + conceptSetUtils, +) { + + class IncludedConceptsPreview extends AutoBind(Component) { + constructor(params) { + super(params); + this.previewConcepts = params.previewConcepts; + this.loading = ko.observable(true); + this.includedConcepts = ko.observableArray(); + this.commonUtils = commonUtils; + this.includedConceptsColumns = conceptSetUtils.getIncludedConceptsColumns({ canEditCurrentConceptSet: ko.observable(false) }, commonUtils, () => {}); + this.includedConceptsColumns.shift(); + this.includedConceptsOptions = conceptSetUtils.includedConceptsOptions; + this.tableOptions = params.tableOptions || commonUtils.getTableOptions('M'); + this.includedDrawCallback = conceptSetUtils.getIncludedConceptSetDrawCallback(this.includedConceptsColumns, { + current: ko.observable({expression: { items: this.previewConcepts }}), + selectedConceptsIndex: ko.pureComputed(() => { + const index = this.previewConcepts() + .reduce((result, item) => { + const itemArr = result[item.concept.CONCEPT_ID] || []; + itemArr.push(item); + result[item.concept.CONCEPT_ID] = itemArr; + return result; + }, {}); + return index || {}; + }), + includedConceptsMap: ko.pureComputed( + () => this.includedConcepts().reduce((result, item) => { + result[item.CONCEPT_ID] = item; + return result; + }, {}) + ) + }); + + this.subscriptions.push(ko.pureComputed(() => ko.toJSON(this.previewConcepts())) + .extend({ + rateLimit: { + timeout: 1000, + method: "notifyWhenChangesStop" + } + }) + .subscribe(this.loadIncluded)); + this.loadIncluded(); + } + + async loadIncluded() { + try { + this.loading(true); + const conceptIds = await vocabularyService.resolveConceptSetExpression({ + items: this.previewConcepts() + }); + const response = await vocabularyService.getConceptsById(conceptIds); + await vocabularyService.loadDensity(response.data); + this.includedConcepts((response.data || []).map(item => ({ + ...item, + ANCESTORS: null, + isSelected: ko.observable(false) + }))); + } catch (err) { + console.error(err); + } finally { + this.loading(false); + } + } + } + + return commonUtils.build('conceptset-list-included-preview', IncludedConceptsPreview, view); +}); \ No newline at end of file diff --git a/js/components/conceptAddBox/preview/included-sourcecodes-preview.html b/js/components/conceptAddBox/preview/included-sourcecodes-preview.html new file mode 100644 index 000000000..83a876b45 --- /dev/null +++ b/js/components/conceptAddBox/preview/included-sourcecodes-preview.html @@ -0,0 +1,11 @@ + +
+ +
\ No newline at end of file diff --git a/js/components/conceptAddBox/preview/included-sourcecodes-preview.js b/js/components/conceptAddBox/preview/included-sourcecodes-preview.js new file mode 100644 index 000000000..1a669047f --- /dev/null +++ b/js/components/conceptAddBox/preview/included-sourcecodes-preview.js @@ -0,0 +1,74 @@ +define([ + 'knockout', + 'text!./included-sourcecodes-preview.html', + 'components/Component', + 'utils/AutoBind', + 'utils/CommonUtils', + 'atlas-state', + 'const', + 'services/ConceptSet', + 'services/Vocabulary', + 'components/conceptset/utils', +], function( + ko, + view, + Component, + AutoBind, + commonUtils, + sharedState, + globalConstants, + conceptSetService, + vocabularyService, + conceptSetUtils, +){ + + class IncludedSourcecodesPreview extends AutoBind(Component) { + constructor(params) { + super(params); + this.previewConcepts = params.previewConcepts; + this.loading = ko.observable(true); + this.includedSourcecodes = ko.observableArray(); + this.relatedSourcecodesColumns = globalConstants.getRelatedSourcecodesColumns(sharedState, { canEditCurrentConceptSet: ko.observable(false) }, () => {}) + .filter(c => + c.data === 'CONCEPT_ID' || + c.data === 'CONCEPT_CODE' || + c.data === 'CONCEPT_NAME' || + c.data === 'CONCEPT_CLASS_ID' || + c.data === 'DOMAIN_ID' || + c.data === 'VOCABULARY_ID' + ); + this.relatedSourcecodesOptions = globalConstants.relatedSourcecodesOptions; + this.tableOptions = params.tableOptions || commonUtils.getTableOptions('M'); + + this.subscriptions.push(ko.pureComputed(() => ko.toJSON(this.previewConcepts())) + .extend({ + rateLimit: { + timeout: 1000, + method: "notifyWhenChangesStop" + } + }) + .subscribe(this.loadSourceCodes)); + this.loadSourceCodes(); + } + + async loadSourceCodes() { + try { + this.loading(true); + const conceptIds = await vocabularyService.resolveConceptSetExpression({ + items: this.previewConcepts() + }); + const data = await vocabularyService.getMappedConceptsById(conceptIds); + this.includedSourcecodes(data.map(item => ({ + ...item, + isSelected: ko.observable(false), + }))); + } catch (err) { + console.error(err); + } finally { + this.loading(false); + } + } + } + + return commonUtils.build('conceptset-list-included-sourcecodes-preview', IncludedSourcecodesPreview, view); +}); \ No newline at end of file diff --git a/js/components/conceptset/ConceptSetStore.js b/js/components/conceptset/ConceptSetStore.js index b8d7472c4..b3dfa0685 100644 --- a/js/components/conceptset/ConceptSetStore.js +++ b/js/components/conceptset/ConceptSetStore.js @@ -93,6 +93,8 @@ define([ .extend({ rateLimit: { timeout: 500, method: "notifyWhenChangesStop" } }); this.isEditable = ko.observable(false); + + this.currentConseptSetTab = ko.observable(''); } clear() { @@ -130,6 +132,7 @@ define([ } async refresh(mode) { + this.currentConseptSetTab(mode); if (this.resolvingConceptSetExpression() || this.conceptSetInclusionIdentifiers() == null) // do nothing return false; switch (mode) { @@ -175,6 +178,7 @@ define([ const identifiers = concepts.map(c => c.CONCEPT_ID); try { const data = await vocabularyService.getMappedConceptsById(identifiers); + await vocabularyService.loadDensity(data); const normalizedData = data.map(item => ({ ...item, isSelected: ko.observable(false), diff --git a/js/components/conceptset/conceptset-list.js b/js/components/conceptset/conceptset-list.js index 392da05e0..76ab920ef 100644 --- a/js/components/conceptset/conceptset-list.js +++ b/js/components/conceptset/conceptset-list.js @@ -46,6 +46,7 @@ define([ this.conceptSets = params.conceptSets; this.exportCSV = typeof params.exportCSV !== 'undefined' ? params.exportCSV : true; this.conceptSetStore = params.conceptSetStore; + this.selectedSource = ko.observable(); this.canEdit = params.canEdit || (() => false); this.exportConceptSets = params.exportConceptSets || (() => false); this.currentConceptSet = this.conceptSetStore.current; @@ -85,6 +86,7 @@ define([ ...params, tableOptions, conceptSetStore: this.conceptSetStore, + selectedSource: this.selectedSource, activeConceptSet: ko.observable(this.conceptSetStore), // addConceptBox expectes an observable for activeConceptSet currentConceptSet: this.conceptSetStore.current, loadConceptSet: this.loadConceptSet, diff --git a/js/components/conceptset/import/identifiers.html b/js/components/conceptset/import/identifiers.html index 4c04d3fd8..dcd0aa328 100644 --- a/js/components/conceptset/import/identifiers.html +++ b/js/components/conceptset/import/identifiers.html @@ -4,6 +4,7 @@
\ No newline at end of file diff --git a/js/components/conceptset/import/sourcecodes.html b/js/components/conceptset/import/sourcecodes.html index 2db90bed3..605b2463e 100644 --- a/js/components/conceptset/import/sourcecodes.html +++ b/js/components/conceptset/import/sourcecodes.html @@ -25,7 +25,7 @@
diff --git a/js/components/conceptset/import/sourcecodes.js b/js/components/conceptset/import/sourcecodes.js index c2bb51963..849dc0c80 100644 --- a/js/components/conceptset/import/sourcecodes.js +++ b/js/components/conceptset/import/sourcecodes.js @@ -117,6 +117,10 @@ define([ }; } + getSelectedConcepts() { + return commonUtils.getSelectedConcepts(this.loadedConcepts); + } + async runImport(options) { this.appendConcepts(this.loadedConcepts().filter(c => c.isSelected()), options); } diff --git a/js/components/conceptset/included-sourcecodes.html b/js/components/conceptset/included-sourcecodes.html index 9d4bb54b7..8b6fc3bd1 100644 --- a/js/components/conceptset/included-sourcecodes.html +++ b/js/components/conceptset/included-sourcecodes.html @@ -1,5 +1,6 @@
+
\ No newline at end of file diff --git a/js/components/conceptset/included-sourcecodes.js b/js/components/conceptset/included-sourcecodes.js index db13b691c..c1b4e13e2 100644 --- a/js/components/conceptset/included-sourcecodes.js +++ b/js/components/conceptset/included-sourcecodes.js @@ -9,6 +9,7 @@ define([ 'services/ConceptSet', './utils', 'components/conceptAddBox/concept-add-box', + 'components/dataSourceSelect' ], function( ko, view, @@ -27,7 +28,8 @@ define([ this.loading = params.loading; this.canEdit = params.canEdit; this.conceptSetStore = params.conceptSetStore; - + this.selectedSource = params.selectedSource; + this.includedSourcecodes = this.conceptSetStore.includedSourcecodes; this.relatedSourcecodesColumns = globalConstants.getRelatedSourcecodesColumns(sharedState, { canEditCurrentConceptSet: this.canEdit }, (data, selected) => { const conceptIds = data.map(c => c.CONCEPT_ID); @@ -35,15 +37,17 @@ define([ this.includedSourcecodes.valueHasMutated(); }); this.relatedSourcecodesOptions = globalConstants.relatedSourcecodesOptions; - this.includedSourcecodes = this.conceptSetStore.includedSourcecodes; this.tableOptions = params.tableOptions || commonUtils.getTableOptions('M'); this.canAddConcepts = ko.pureComputed(() => this.includedSourcecodes() && this.includedSourcecodes().some(item => item.isSelected())); } + getSelectedConcepts() { + return ko.unwrap(this.includedSourcecodes) && commonUtils.getSelectedConcepts(this.includedSourcecodes); + } + addConcepts(options) { this.conceptSetStore.loadingSourceCodes(true); - const concepts = commonUtils.getSelectedConcepts(this.includedSourcecodes); - const items = commonUtils.buildConceptSetItems(concepts, options); + const items = commonUtils.buildConceptSetItems(this.getSelectedConcepts(), options); conceptSetUtils.addItemsToConceptSet({ items, conceptSetStore: this.conceptSetStore, diff --git a/js/components/conceptset/included.html b/js/components/conceptset/included.html index 6cfa41b3c..777217311 100644 --- a/js/components/conceptset/included.html +++ b/js/components/conceptset/included.html @@ -1,5 +1,6 @@
+ -
- + + \ No newline at end of file diff --git a/js/components/conceptset/included.js b/js/components/conceptset/included.js index 4c505fa9e..2973461a6 100644 --- a/js/components/conceptset/included.js +++ b/js/components/conceptset/included.js @@ -7,9 +7,9 @@ define([ 'atlas-state', 'services/ConceptSet', './utils', - 'const', 'components/conceptAddBox/concept-add-box', - './concept-modal' + './concept-modal', + 'components/dataSourceSelect' ], function ( ko, view, @@ -19,7 +19,6 @@ define([ sharedState, conceptSetService, conceptSetUtils, - globalConstants, ) { class IncludedConcepts extends AutoBind(Component){ @@ -27,6 +26,7 @@ define([ super(params); this.canEdit = params.canEdit; this.conceptSetStore = params.conceptSetStore; + this.selectedSource = params.selectedSource; this.includedConcepts = this.conceptSetStore.includedConcepts; this.commonUtils = commonUtils; this.loading = params.loading; @@ -46,21 +46,23 @@ define([ ancestors: this.ancestors, ancestorsModalIsShown: this.ancestorsModalIsShown }); - this.includedDrawCallback = conceptSetUtils.getIncludedConceptSetDrawCallback(this.includedConceptsColumns, this.conceptSetStore); - + + } + + getSelectedConcepts() { + return ko.unwrap(this.includedConcepts) && commonUtils.getSelectedConcepts(this.includedConcepts); } addConcepts(options) { this.conceptSetStore.loadingIncluded(true); - const concepts = commonUtils.getSelectedConcepts(this.includedConcepts); - const items = commonUtils.buildConceptSetItems(concepts, options); + const items = commonUtils.buildConceptSetItems(this.getSelectedConcepts(), options); conceptSetUtils.addItemsToConceptSet({ items, conceptSetStore: this.conceptSetStore, }); commonUtils.clearConceptsSelectionState(this.includedConcepts); - } + } } diff --git a/js/components/conceptset/recommend.html b/js/components/conceptset/recommend.html index bd295c3b6..f1b4983d3 100644 --- a/js/components/conceptset/recommend.html +++ b/js/components/conceptset/recommend.html @@ -1,5 +1,6 @@
+
diff --git a/js/components/conceptset/recommend.js b/js/components/conceptset/recommend.js index 453e235b6..0e7f7e6e0 100644 --- a/js/components/conceptset/recommend.js +++ b/js/components/conceptset/recommend.js @@ -7,6 +7,7 @@ define([ 'atlas-state', './utils', 'components/conceptAddBox/concept-add-box', + 'components/dataSourceSelect' ], function( ko, view, @@ -23,6 +24,7 @@ define([ this.loading = params.loading; this.canEdit = params.canEdit; this.conceptSetStore = params.conceptSetStore; + this.selectedSource = params.selectedSource; this.recommendedConcepts = this.conceptSetStore.recommendedConcepts; this.recommendedConceptOptions = ko.pureComputed(() => { @@ -40,6 +42,10 @@ define([ this.canAddConcepts = ko.pureComputed(() => this.recommendedConcepts() && this.recommendedConcepts().some(item => item.isSelected())); } + getSelectedConcepts() { + return commonUtils.getSelectedConcepts(this.recommendedConcepts); + } + addConcepts(options) { this.conceptSetStore.loadingRecommended(true); const concepts = commonUtils.getSelectedConcepts(this.recommendedConcepts); diff --git a/js/components/conceptset/utils.js b/js/components/conceptset/utils.js index 1aaad298a..bd16df853 100644 --- a/js/components/conceptset/utils.js +++ b/js/components/conceptset/utils.js @@ -58,6 +58,16 @@ define(['knockout','utils/CommonUtils', 'utils/Renderers', 'services/http','atla data: 'DESCENDANT_RECORD_COUNT', className: 'numeric' }, + { + title: ko.i18n('columns.pc', 'PC'), + data: 'PERSON_COUNT', + className: 'numeric', + }, + { + title: ko.i18n('columns.dpc', 'DPC'), + data: 'DESCENDANT_PERSON_COUNT', + className: 'numeric', + }, { title: ko.i18n('columns.domain', 'Domain'), data: 'DOMAIN_ID' @@ -159,6 +169,16 @@ define(['knockout','utils/CommonUtils', 'utils/Renderers', 'services/http','atla data: 'DESCENDANT_RECORD_COUNT', className: 'numeric' }, + { + title: ko.i18n('columns.pc', 'PC'), + data: 'PERSON_COUNT', + className: 'numeric', + }, + { + title: ko.i18n('columns.dpc', 'DPC'), + data: 'DESCENDANT_PERSON_COUNT', + className: 'numeric', + }, { title: ko.i18n('columns.domain', 'Domain'), data: 'DOMAIN_ID' diff --git a/js/components/cyclops/components/control-editor.html b/js/components/cyclops/components/control-editor.html index d348f926b..7ee3a0855 100644 --- a/js/components/cyclops/components/control-editor.html +++ b/js/components/cyclops/components/control-editor.html @@ -55,7 +55,7 @@
- +
diff --git a/js/components/dataSourceSelect.html b/js/components/dataSourceSelect.html new file mode 100644 index 000000000..cbe55bfa7 --- /dev/null +++ b/js/components/dataSourceSelect.html @@ -0,0 +1,13 @@ +
+ + + + +
\ No newline at end of file diff --git a/js/components/dataSourceSelect.js b/js/components/dataSourceSelect.js new file mode 100644 index 000000000..3ba6b70ef --- /dev/null +++ b/js/components/dataSourceSelect.js @@ -0,0 +1,108 @@ +define([ + 'knockout', + 'text!./dataSourceSelect.html', + 'components/Component', + 'utils/AutoBind', + 'utils/CommonUtils', + 'atlas-state', + 'services/AuthAPI', + 'services/Vocabulary', + 'components/conceptset/const' +], function ( + ko, + view, + Component, + AutoBind, + commonUtils, + sharedState, + authApi, + vocabularyProvider, + constants +) { + + class DataSourceSelect extends AutoBind(Component){ + constructor(params) { + super(params); + this.tab = params.tab; + this.conceptSetStore = params.conceptSetStore; + this.includedConcepts = this.conceptSetStore.includedConcepts; + this.includedSourcecodes = this.conceptSetStore.includedSourcecodes; + this.recommendedConcepts = this.conceptSetStore.recommendedConcepts; + this.currentConceptSetTab = this.conceptSetStore.currentConseptSetTab; + if (this.tab) { + this.subscriptions.push(this.currentConceptSetTab.subscribe((tab) => { + // refresh counts if the source was changed in another tab + if (this.tab === tab) { + this.refreshRecordCounts(); + } + })); + } + this.commonUtils = commonUtils; + this.loading = params.loading; + this.selectedSource = params.selectedSource || ko.observable(); + this.currentSourceId = this.selectedSource() && this.selectedSource().sourceId; + this.selectedSourceValue = ko.pureComputed(() => { + return this.selectedSource() && this.selectedSource().sourceId; + }); + this.resultSources = ko.computed(() => { + const resultSources = []; + sharedState.sources().forEach((source) => { + if (source.hasResults && authApi.isPermittedAccessSource(source.sourceKey)) { + resultSources.push(source); + if (source.resultsUrl === sharedState.resultsUrl() && !this.selectedSource()) { + this.selectedSource(source); + this.currentSourceId = source.sourceId; + } + } + }) + return resultSources; + }); + + this.recordCountsRefreshing = ko.observable(false); + this.recordCountClass = ko.pureComputed(() => { + return this.recordCountsRefreshing() ? "fa fa-circle-notch fa-spin fa-lg" : "fa fa-database fa-lg"; + }); + } + + refreshRecordCountsHandler(obj, event) { + if (!event.originalEvent) { + return; + } + this.recordCountsRefreshing(true); + const currentResultSource = this.resultSources().find(source => source.sourceId == event.target.value) + this.selectedSource(currentResultSource); + this.refreshRecordCounts(); + } + + async refreshRecordCounts() { + if (this.selectedSource().sourceId === this.currentSourceId) { + this.recordCountsRefreshing(false); + return; + } + + const { ViewMode } = constants; + switch (this.currentConceptSetTab()) { + case ViewMode.INCLUDED: + const resultsIncludedConcepts = this.includedConcepts(); + await vocabularyProvider.loadDensity(resultsIncludedConcepts, this.selectedSource().sourceKey); + this.includedConcepts(resultsIncludedConcepts); + break; + case ViewMode.SOURCECODES: + const resultsIncludedSourcecodes = this.includedSourcecodes(); + await vocabularyProvider.loadDensity(resultsIncludedSourcecodes, this.selectedSource().sourceKey); + this.includedSourcecodes(resultsIncludedSourcecodes); + break; + case ViewMode.RECOMMEND: + const resultsRecommendedConcepts = this.recommendedConcepts(); + await vocabularyProvider.loadDensity(resultsRecommendedConcepts, this.selectedSource().sourceKey); + this.recommendedConcepts(resultsRecommendedConcepts); + break; + } + + this.currentSourceId = this.selectedSource().sourceId; + this.recordCountsRefreshing(false); + } + } + + return commonUtils.build('datasource-select', DataSourceSelect, view); +}); \ No newline at end of file diff --git a/js/components/faceted-datatable.js b/js/components/faceted-datatable.js index e72087db4..74f963074 100644 --- a/js/components/faceted-datatable.js +++ b/js/components/faceted-datatable.js @@ -66,6 +66,8 @@ define(['knockout', 'text!./faceted-datatable.html', 'crossfilter', 'utils/Commo self.scrollY = params.scrollY || null; self.scrollCollapse = params.scrollCollapse || false; + self.outsideFilters = (params.outsideFilters || ko.observable()).extend({notify: 'always'}); + self.updateFilters = function (data, event) { var facet = data.facet; data.selected(!data.selected()); @@ -88,8 +90,22 @@ define(['knockout', 'text!./faceted-datatable.html', 'crossfilter', 'utils/Commo }); } self.data.valueHasMutated(); + + if (params?.updateLastSelectedMatchFilter) { + params.updateLastSelectedMatchFilter(data.key); + } } + + self.updateOutsideFilters = function (key) { + const facets = self.facets(); + const facetItems = facets.map(facet => facet.facetItems); + const selectedItemIndex = facetItems.findIndex(facet => facet.find(el => el.key === key)); + const selectedFacet = facets[selectedItemIndex].facetItems.find(facet => facet.key === key); + + self.updateFilters({...selectedFacet}); + }; + // additional helper function to help with crossfilter-ing dimensions that contain nulls self.facetDimensionHelper = function facetDimensionHelper(val) { var ret = val === null ? self.nullFacetLabel : val; @@ -144,6 +160,13 @@ define(['knockout', 'text!./faceted-datatable.html', 'crossfilter', 'utils/Commo }) ); + subscriptions.push( + self.outsideFilters.subscribe(function (newValue) { + if (self.outsideFilters() != undefined) { + self.updateOutsideFilters(newValue); + } + }) + ); // init component if (ko.isComputed(self.reference)) { // valueHasMutated doesn't work for computed diff --git a/js/components/feedback.html b/js/components/feedback.html index eb98398d4..ba86da975 100644 --- a/js/components/feedback.html +++ b/js/components/feedback.html @@ -4,6 +4,7 @@
- +
+
\ No newline at end of file diff --git a/js/components/feedback.js b/js/components/feedback.js index 07928aa92..54e8c1479 100644 --- a/js/components/feedback.js +++ b/js/components/feedback.js @@ -3,6 +3,8 @@ define(['knockout', 'text!./feedback.html', 'appConfig'], function (ko, view, co var self = this; self.supportMail = config.supportMail; self.supportMailRef = "mailto:" + config.supportMail; + self.contacts = config.feedbackContacts; + self.feedbackTemplate = config.feedbackCustomHtmlTemplate; } var component = { viewModel: feedback, diff --git a/js/pages/data-sources/classes/Report.js b/js/components/reports/classes/Report.js similarity index 94% rename from js/pages/data-sources/classes/Report.js rename to js/components/reports/classes/Report.js index 6bf9222c7..5d73a0696 100644 --- a/js/pages/data-sources/classes/Report.js +++ b/js/components/reports/classes/Report.js @@ -1,6 +1,6 @@ define([ 'knockout', - 'pages/data-sources/const', + 'components/reports/const', 'services/http', 'components/Component', ], function ( @@ -14,7 +14,6 @@ define([ super(params); this.isLoading = ko.observable(true); this.chartFormats = {}; - this.context = params.context; this.source = this.context.currentSource; this.title = ko.computed(() => { @@ -35,9 +34,10 @@ define([ } getData() { + const url = constants.apiPaths.report({ sourceKey: this.source() ? this.source().sourceKey : this.context.routerParams.sourceKey, - path: this.context.routerParams.reportName, + path: this.context.currentReport().path || this.context.routerParams.reportName, conceptId: this.conceptId, }); this.context.loadingReport(true); diff --git a/js/pages/data-sources/classes/Treemap.js b/js/components/reports/classes/Treemap.js similarity index 99% rename from js/pages/data-sources/classes/Treemap.js rename to js/components/reports/classes/Treemap.js index acf8c77e0..5a471d3d5 100644 --- a/js/pages/data-sources/classes/Treemap.js +++ b/js/components/reports/classes/Treemap.js @@ -24,6 +24,7 @@ define([ // abstract, no need to define component name here constructor(params) { + super(params); this.treeData = ko.observable(); this.tableData = ko.observable(); diff --git a/js/components/reports/const.js b/js/components/reports/const.js new file mode 100644 index 000000000..981e7a36a --- /dev/null +++ b/js/components/reports/const.js @@ -0,0 +1,97 @@ +define( + (require, factory) => { + const ko = require('knockout'); + const config = require('appConfig'); + + const apiPaths = { + report: ({ sourceKey, path, conceptId }) => `${config.api.url}cdmresults/${sourceKey}/${path}${conceptId !== null ? `/${conceptId}` : ''}`, + }; + + // aggregate property descriptors + const recordsPerPersonProperty = { + name: "recordsPerPerson", + description: ko.i18n('dataSources.const.recordsPerPerson', 'Records per person') + }; + const lengthOfEraProperty = { + name: "lengthOfEra", + description: ko.i18n('dataSources.const.lengthOfEra', 'Length of era') + }; + + const reports = [{ + name: ko.i18n('dataSources.reports.dashboard', 'Dashboard'), + path: "dashboard", + component: "report-dashboard", + summary: ko.observable() + }, + { + name: ko.i18n('dataSources.reports.dataDensity', 'Data Density'), + path: "datadensity", + component: "report-datadensity", + }, + { + name: ko.i18n('dataSources.reports.person', 'Person'), + path: "person", + component: "report-person", + }, + { + name: ko.i18n('dataSources.reports.visit', 'Visit'), + path: "visit", + component: "report-visit", + }, + { + name: ko.i18n('dataSources.reports.conditionOccurrence', 'Condition Occurrence'), + path: "condition", + component: "report-condition", + }, + { + name: ko.i18n('dataSources.reports.conditionEra', 'Condition Era'), + path: "conditionera", + component: "report-condition-era", + }, + { + name: ko.i18n('dataSources.reports.procedure', 'Procedure'), + path: "procedure", + component: "report-procedure", + }, + { + name: ko.i18n('dataSources.reports.drugExposure', 'Drug Exposure'), + path: "drug", + component: "report-drug", + }, + { + name: ko.i18n('dataSources.reports.drugEra', 'Drug Era'), + path: "drugera", + component: "report-drug-era", + }, + { + name: ko.i18n('dataSources.reports.measurement', 'Measurement'), + path: "measurement", + component: "report-measurement", + }, + { + name: ko.i18n('dataSources.reports.observation', 'Observation'), + path: "observation", + component: "report-observation", + }, + { + name: ko.i18n('dataSources.reports.observationPeriod', 'Observation Period'), + path: "observationPeriod", + component: "report-observation-period" + }, + { + name: ko.i18n('dataSources.reports.death', 'Death'), + path: "death", + component: "report-death", + } + ]; + + return { + apiPaths, + aggProperties: { + byPerson: recordsPerPersonProperty, + byLengthOfEra: lengthOfEraProperty, + }, + reports, + }; + } +); \ No newline at end of file diff --git a/js/pages/data-sources/components/reports/treemapDrilldown.html b/js/components/reports/reportDrilldown.html similarity index 94% rename from js/pages/data-sources/components/reports/treemapDrilldown.html rename to js/components/reports/reportDrilldown.html index 29d405f56..a60f39a35 100644 --- a/js/pages/data-sources/components/reports/treemapDrilldown.html +++ b/js/components/reports/reportDrilldown.html @@ -1,10 +1,15 @@ + + + + + -
- +
+ scroll to the top diff --git a/js/pages/data-sources/components/reports/treemapDrilldown.js b/js/components/reports/reportDrilldown.js similarity index 85% rename from js/pages/data-sources/components/reports/treemapDrilldown.js rename to js/components/reports/reportDrilldown.js index 9c6234435..9fb21448a 100644 --- a/js/pages/data-sources/components/reports/treemapDrilldown.js +++ b/js/components/reports/reportDrilldown.js @@ -1,12 +1,12 @@ define([ 'knockout', - 'text!./treemapDrilldown.html', + 'text!./reportDrilldown.html', 'd3', 'atlascharts', 'utils/CommonUtils', 'utils/ChartUtils', 'const', - 'pages/data-sources/classes/Report', + './classes/Report', 'components/Component', 'components/charts/histogram', 'components/charts/line', @@ -25,10 +25,9 @@ define([ commonUtils, ChartUtils, constants, - Report, - Component + Report ) { - class TreemapDrilldown extends Report { + class ReportDrilldown extends Report { constructor(params) { super(params); @@ -36,6 +35,7 @@ define([ name: '', }); this.isError = ko.observable(false); + this.hideReportName = ko.observable(params.hideReportName || false); // options this.byFrequency = false; @@ -145,7 +145,7 @@ define([ }, }; - this.currentReport = params.currentReport; + this.currentReport = params.currentReport(); this.byFrequency = params.byFrequency; this.byUnit = params.byUnit; this.byType = params.byType; @@ -154,13 +154,23 @@ define([ this.byQualifier = params.byQualifier; this.byLengthOfEra = params.byLengthOfEra; this.context = params.context; + this.refreshReport = !!params.refreshReport; this.subscriptions.push(params.currentConcept.subscribe(this.loadData.bind(this))); + + if (params.currentSource) { + this.subscriptions.push(params.currentSource.subscribe(newValue => { + if (newValue && this.refreshReport) { + this.loadData(params.currentConcept()); + } + }) + )}; this.loadData(params.currentConcept()); - this.reportName = ko.computed(() => `${this.currentReport.name()}_${this.currentConcept().name}`); + this.reportName = ko.computed(() => this.currentConcept().name ? `${this.currentReport.name()}_${this.currentConcept().name}`: `${this.currentReport.name()}`); + this.isData= ko.observable(true); } parseAgeData(rawAgeData) { - this.ageData(this.parseBoxplotData(rawAgeData).data); + this.ageData(this.parseBoxplotData(rawAgeData)?.data); } parseLengthOfEra(rawLengthOfEra) { @@ -178,18 +188,26 @@ define([ this.chartFormats.prevalenceByMonth.xScale = d3.scaleTime() .domain(d3.extent(byMonthSeries[0].values, d => d.xValue)); this.prevalenceByMonthData(byMonthSeries); + } else { + this.prevalenceByMonthData(null); } } parsePrevalenceByType(rawPrevalenceByType) { if (!!rawPrevalenceByType && rawPrevalenceByType.length > 0) { this.prevalenceByTypeData(ChartUtils.mapConceptData(rawPrevalenceByType)); + } else { + this.prevalenceByTypeData(null); } } parsePrevalenceByGenderAgeYear(rawPrevalenceByGenderAgeYear) { - this.chartFormats.prevalenceByGenderAgeYear.trellisSet = constants.defaultDeciles; - this.prevalenceByGenderAgeYearData(rawPrevalenceByGenderAgeYear); + if (rawPrevalenceByGenderAgeYear) { + this.chartFormats.prevalenceByGenderAgeYear.trellisSet = constants.defaultDeciles; + this.prevalenceByGenderAgeYearData(rawPrevalenceByGenderAgeYear); + } else { + this.prevalenceByGenderAgeYearData(null); + } } parseFrequencyDistribution(rawData, report) { @@ -223,6 +241,8 @@ define([ const freqHistData = atlascharts.histogram.mapHistogram(frequencyHistogram); this.frequencyDistributionData(freqHistData); } + } else { + this.frequencyDistributionData(null); } } @@ -281,7 +301,7 @@ define([ this.parsePrevalenceByType(data.byType); this.parsePrevalenceByGenderAgeYear(data.prevalenceByGenderAgeYear); if (this.byFrequency) { - this.parseFrequencyDistribution(data.frequencyDistribution, this.currentReport.name); + this.parseFrequencyDistribution(data.frequencyDistribution, this.currentReport.name()); } if (this.byValueAsConcept) { @@ -324,6 +344,10 @@ define([ } } + checkData(data) { + const isData = Object.values(data).find(item => !!item.length); + this.isData(!!isData); + } getData() { const response = super.getData(); return response; @@ -333,23 +357,29 @@ define([ if (!selectedConcept) { return; } - this.conceptId = selectedConcept.concept_id; + + this.context.loadingDrilldownDone(false); + this.conceptId = selectedConcept.concept_id !== undefined ? selectedConcept.concept_id : selectedConcept.CONCEPT_ID; this.currentConcept(selectedConcept); this.isError(false); this.getData() .then((data) => { + this.checkData(data.data); this.parseData(data); this.context.loadingDrilldownDone(true); this.context.showLoadingDrilldownModal(false); - setTimeout(() => document.getElementById('drilldownReport').scrollIntoView(), 0); + if (!this.hideReportName()) { + setTimeout(() => document.getElementById('drilldownReport').scrollIntoView(), 0); + } }) .catch((er) => { this.isError(true); console.error(er); + this.context.loadingDrilldownDone(true); this.context.showLoadingDrilldownModal(false); }); } } - return commonUtils.build('report-treemap-drilldown', TreemapDrilldown, view); -}); + return commonUtils.build('report-drilldown', ReportDrilldown, view); +}); \ No newline at end of file diff --git a/js/components/security/access/configure-access-modal.html b/js/components/security/access/configure-access-modal.html index 65af8a6af..fb966053a 100644 --- a/js/components/security/access/configure-access-modal.html +++ b/js/components/security/access/configure-access-modal.html @@ -3,46 +3,80 @@ data: { classes: classes, isLoading: isLoading, - roleName: roleName, - columns: columns, - accessList: accessList, + readRoleName: readRoleName, + writeRoleName: writeRoleName, + writeAccessColumns: writeAccessColumns, + readAccessColumns: readAccessColumns, + writeAccessList: writeAccessList, + readAccessList: readAccessList, grantAccess: grantAccess, revokeRoleAccess: revokeRoleAccess, - roleOptions: roleOptions, - roleSearch: roleSearch + readRoleOptions: readRoleOptions, + readRoleSearch: readRoleSearch, + writeRoleOptions: writeRoleOptions, + writeRoleSearch: writeRoleSearch }">
- +
- +
- +
- + +
+ +
+ + + + +
+
+ +
+
+ + + - \ No newline at end of file + diff --git a/js/components/security/access/configure-access-modal.js b/js/components/security/access/configure-access-modal.js index b45fb055b..61efbe232 100644 --- a/js/components/security/access/configure-access-modal.js +++ b/js/components/security/access/configure-access-modal.js @@ -19,13 +19,20 @@ define([ this.isModalShown = params.isModalShown; this.isLoading = ko.observable(false); - this.accessList = ko.observable([]); - this.roleName = ko.observable(); - this.roleSuggestions = ko.observable([]); - this.roleOptions = ko.computed(() => this.roleSuggestions().map(r => r.name)); - this.roleSearch = ko.observable(); - this.roleSearch.subscribe(str => this.loadRoleSuggestions(str)); + this.writeRoleName = ko.observable(); + this.writeAccessList = ko.observable([]); + this.writeRoleSuggestions = ko.observable([]); + this.writeRoleOptions = ko.computed(() => this.writeRoleSuggestions().map(r => r.name)); + this.writeRoleSearch = ko.observable(); + this.writeRoleSearch.subscribe(str => this.loadWriteRoleSuggestions(str)); + + this.readAccessList = ko.observable([]); + this.readRoleName = ko.observable(); + this.readRoleSuggestions = ko.observable([]); + this.readRoleOptions = ko.computed(() => this.readRoleSuggestions().map(r => r.name)); + this.readRoleSearch = ko.observable(); + this.readRoleSearch.subscribe(str => this.loadReadRoleSuggestions(str)); this.isOwnerFn = params.isOwnerFn; this.grantAccessFn = params.grantAccessFn; @@ -33,20 +40,38 @@ define([ this.revokeAccessFn = params.revokeAccessFn; this.loadRoleSuggestionsFn = params.loadRoleSuggestionsFn; - this.columns = [ + this.readAccessColumns = [ + { + class: this.classes('access-tbl-col-id'), + title: ko.i18n('readAccessColumns.id', 'ID'), + data: 'id' + }, + { + class: this.classes('access-tbl-col-name'), + title: ko.i18n('readAccessColumns.name', 'Name'), + data: 'name' + }, + { + class: this.classes('access-tbl-col-action'), + title: ko.i18n('readAccessColumns.action', 'Action'), + render: (s, p, d) => !this.isOwnerFn(d.name) ? `` : '-' + } + ]; + + this.writeAccessColumns = [ { class: this.classes('access-tbl-col-id'), - title: ko.i18n('columns.id', 'ID'), + title: ko.i18n('writeAccessColumns.id', 'ID'), data: 'id' }, { class: this.classes('access-tbl-col-name'), - title: ko.i18n('columns.name', 'Name'), + title: ko.i18n('writeAccessColumns.name', 'Name'), data: 'name' }, { class: this.classes('access-tbl-col-action'), - title: ko.i18n('columns.action', 'Action'), + title: ko.i18n('writeAccessColumns.action', 'Action'), render: (s, p, d) => !this.isOwnerFn(d.name) ? `` : '-' } ]; @@ -54,45 +79,64 @@ define([ this.isModalShown.subscribe(open => !!open && this.loadAccessList()); } - async _loadAccessList() { - let accessList = await this.loadAccessListFn(); - accessList = accessList.map(a => ({ ...a, revoke: () => this.revokeRoleAccess(a.id) })); - this.accessList(accessList); + async _loadReadAccessList() { + let accessList = await this.loadAccessListFn('READ'); + accessList = accessList.map(a => ({ ...a, revoke: () => this.revokeRoleAccess(a.id, 'READ') })); + this.readAccessList(accessList); + } + + async _loadWriteAccessList() { + let accessList = await this.loadAccessListFn('WRITE'); + accessList = accessList.map(a => ({ ...a, revoke: () => this.revokeRoleAccess(a.id, 'WRITE') })); + this.writeAccessList(accessList); } - async loadRoleSuggestions() { - const res = await this.loadRoleSuggestionsFn(this.roleSearch()); - this.roleSuggestions(res); + async loadReadRoleSuggestions() { + const res = await this.loadRoleSuggestionsFn(this.readRoleSearch()); + this.readRoleSuggestions(res); + } + + async loadWriteRoleSuggestions() { + const res = await this.loadRoleSuggestionsFn(this.writeRoleSearch()); + this.writeRoleSuggestions(res); } async loadAccessList() { - this.isLoading(false); + this.isLoading(true); try { - await this._loadAccessList(); + await this._loadReadAccessList(); + await this._loadWriteAccessList(); } catch (ex) { console.log(ex); } this.isLoading(false); } - async grantAccess() { + async grantAccess(perm_type) { this.isLoading(true); try { - const role = this.roleSuggestions().find(r => r.name === this.roleName()); - await this.grantAccessFn(role.id); - await this._loadAccessList(); - this.roleName(''); + if (perm_type == 'WRITE'){ + const role = this.writeRoleSuggestions().find(r => r.name === this.writeRoleName()); + await this.grantAccessFn(role.id,'WRITE'); + await this._loadWriteAccessList(); + this.writeRoleName(''); + } else { + const role = this.readRoleSuggestions().find(r => r.name === this.readRoleName()); + await this.grantAccessFn(role.id,'READ'); + await this._loadReadAccessList(); + this.readRoleName(''); + } } catch (ex) { console.log(ex); } this.isLoading(false); } - async revokeRoleAccess(roleId) { + async revokeRoleAccess(roleId, perm_type) { this.isLoading(true); - try { - await this.revokeAccessFn(roleId); - await this._loadAccessList(); + try { + await this.revokeAccessFn(roleId, perm_type); + await this.loadAccessList(); } catch (ex) { console.log(ex); } diff --git a/js/components/tags/modal/tags-modal.js b/js/components/tags/modal/tags-modal.js index e303e312c..ed30c5158 100644 --- a/js/components/tags/modal/tags-modal.js +++ b/js/components/tags/modal/tags-modal.js @@ -50,21 +50,21 @@ define([ title: ko.i18n('columns.name', 'Name'), width: '100px', render: (s, p, d) => { - return `${d.name}`; + return `${d.name}`; } }, { title: ko.i18n('columns.description', 'Description'), width: '465px', render: (s, p, d) => { - return `${d.description}`; + return `${d.description || '-'}`; } }, { title: ko.i18n('columns.type', 'Type'), width: '80px', render: (s, p, d) => { - d.typeText = d.type === 'CUSTOM' + d.typeText = d.allowCustom ? ko.i18n('components.tags.typeCustom', 'Free-form') : ko.i18n('components.tags.typeSystem', 'System'); return ``; @@ -86,7 +86,7 @@ define([ title: ko.i18n('columns.name', 'Name'), width: '100px', render: (s, p, d) => { - return `${d.name}`; + return `${d.name}`; } }, { diff --git a/js/components/utilities/sql/sqlExportPanel.html b/js/components/utilities/sql/sqlExportPanel.html index c34c5e332..3f14e7e57 100644 --- a/js/components/utilities/sql/sqlExportPanel.html +++ b/js/components/utilities/sql/sqlExportPanel.html @@ -1,6 +1,33 @@
-

+    
+
+ +

+            
+            
+            

+            
+
+        
+
+
+ + +
+
+
+ +
+
Sql output with editable values
+
+
\ No newline at end of file diff --git a/js/components/utilities/sql/sqlExportPanel.js b/js/components/utilities/sql/sqlExportPanel.js index 7e213d60b..3ca792a68 100644 --- a/js/components/utilities/sql/sqlExportPanel.js +++ b/js/components/utilities/sql/sqlExportPanel.js @@ -4,14 +4,23 @@ define([ 'components/Component', 'utils/AutoBind', 'utils/CommonUtils', + 'utils/HighLightUtils', 'services/CohortDefinition', + 'atlas-state', + 'services/AuthAPI', + './sqlExportPanelConfig', + 'less!./sqlExportPanel.less', ], function ( ko, view, Component, AutoBind, commonUtils, + highlightJS, cohortService, + sharedState, + authApi, + defaultInputParamsValues ) { class SqlExportPanel extends AutoBind(Component) { @@ -21,14 +30,36 @@ define([ super(params); const { sql, templateSql, dialect, clipboardTarget } = params; + this.currentCohort = ko.pureComputed(() =>sharedState.CohortDefinition.current().id()); this.dialect = dialect; this.loading = ko.observable(); - this.sqlText = sql || ko.observable(); + this.sqlText = ko.observable(highlightJS(sql,'sql')); this.templateSql = templateSql || ko.observable(); this.templateSql() && this.translateSql(); + this.currentResultSource = ko.observable(); + this.currentResultSourceValue = ko.pureComputed(() => this.currentResultSource() && this.currentResultSource().sourceKey); + this.resultSources = ko.computed(() => { + const resultSources = []; + sharedState.sources().forEach((source) => { + if (source.hasResults && authApi.isPermittedAccessSource(source.sourceKey)) { + resultSources.push(source); + if (source.resultsUrl === sharedState.resultsUrl()) { + this.currentResultSource(source); + } + } + }) + + return resultSources; + }); + this.sqlParamsList = ko.pureComputed(() => this.calculateSqlParamsList(this.sqlText() || this.templateSql())) + this.sqlParams = ko.observable(this.defaultParamsValue(this.sqlParamsList())); this.clipboardTarget = clipboardTarget; + this.sourceSql = ko.observable(false); + this.paramsTemplateSql = ko.observable(this.sqlText); + + //subscriptions this.subscriptions = []; - this.subscriptions.push(this.templateSql.subscribe(v => !!v && this.translateSql())); + this.subscriptions.push(this.sourceSql.subscribe(v => this.onChangeParamsValue())); } dispose() { @@ -40,12 +71,62 @@ define([ this.loading(true); try { const result = await cohortService.translateSql(this.templateSql(), this.dialect); - this.sqlText(result.data && result.data.targetSQL); + this.sqlText(result.data && highlightJS(result.data.targetSQL, 'sql')); + this.sqlParams(this.defaultParamsValue(this.sqlParamsList())); } finally { this.loading(false); } } } + + calculateSqlParamsList(templateSql) { + if (!templateSql) { // on new cohort template sql does not exist yet + return []; + } + + const regexp = /@[-\w]+/g; + const params = templateSql.match(regexp); + const paramsList = new Set(params); + + return paramsList; + } + + onChangeParamsValue() { + let templateText = this.sqlText(); + this.sqlParams().forEach(currentParam => { + if (!!currentParam.value.length) { + templateText = templateText.replaceAll(currentParam.name, currentParam.value); + } + }); + this.paramsTemplateSql(templateText); + } + + defaultParamsValue(paramsList) { + const daimons = this.currentResultSource().daimons; + const inputParams = []; + paramsList.forEach(param => { + const currentDaimon = daimons.find(daimon => daimon.daimonType === defaultInputParamsValues[param]); + const defaultInput = { + name: param, + value: '' + }; + if (!!currentDaimon) { + defaultInput.value = currentDaimon.tableQualifier; + } else if (param === '@target_cohort_id'){ + defaultInput.value = `${this.currentCohort()}`; + } else { + defaultInput.value = ""; + } + inputParams.push(defaultInput); + }); + return inputParams; + } + + onSourceChange(obj, event) { + this.currentResultSource(this.resultSources().find(source => source.sourceKey === event.target.value)); + this.sqlParams(this.defaultParamsValue(this.sqlParamsList())); + this.onChangeParamsValue(); + } } diff --git a/js/components/utilities/sql/sqlExportPanel.less b/js/components/utilities/sql/sqlExportPanel.less new file mode 100644 index 000000000..4f5d1fd4e --- /dev/null +++ b/js/components/utilities/sql/sqlExportPanel.less @@ -0,0 +1,71 @@ +.flex-container { + display: flex; + width: 100%; + box-sizing: border-box; +} + +.params-container { + width: 20%; + height:100%; + margin-top: 5px; + padding-left: 15px; + position: -webkit-sticky; + position: sticky; + top: 20px; +} +.sql-text { + width: 80%; +} + +.params-source { + color: #003142; + font-weight: 600; + margin-bottom: 10px; + select { + color: #333; + font-weight: 400; + box-shadow: inset 0 1px 1px #00000014; + border-radius: 2px; + border: 1px solid #6d6d6d; + outline: none; + } +} + +.sql-params { + display: flex; + flex-direction: column; + margin-top: 10px; +} + +.sql-params-label { + color: #003142; + font-weight: 600; + margin-bottom: 5px; +} +.sql-params-input { + margin-bottom: 10px; + box-shadow: inset 0 1px 1px #00000014; + border-radius: 2px; + border: 1px solid #6d6d6d; + outline: none; + &:focus { + border-color: #66afe9; + } + &:disabled { + background: #eee; + } +} + +.title { + color: #003142; +} + +.sql-checkbox-container { + display: flex; + font-weight: bold; + margin-top: 16px; + align-items: center; + input { + margin: 0 8px 0 0; + } +} diff --git a/js/components/utilities/sql/sqlExportPanelConfig.js b/js/components/utilities/sql/sqlExportPanelConfig.js new file mode 100644 index 000000000..348761bb7 --- /dev/null +++ b/js/components/utilities/sql/sqlExportPanelConfig.js @@ -0,0 +1,13 @@ + // todo: refactoring hardcode on the backend + // org/ohdsi/webapi/cohortresults/CohortResultsAnalysisRunner.java:1625 + // add the request that get these params from backend +define([], () => { + const defaultInputParamsValues = { + '@vocabulary_database_schema': 'Vocabulary', + '@cdm_database_schema': 'CDM', + '@target_database_schema':'Results' + }; + + return defaultInputParamsValues; + +}) \ No newline at end of file diff --git a/js/components/welcome.html b/js/components/welcome.html index 7aeb0d2f0..1ea0812fb 100644 --- a/js/components/welcome.html +++ b/js/components/welcome.html @@ -30,8 +30,8 @@
- - + +
diff --git a/js/components/welcome.js b/js/components/welcome.js index 508567e2a..8a1360c15 100644 --- a/js/components/welcome.js +++ b/js/components/welcome.js @@ -3,7 +3,7 @@ define([ 'text!./welcome.html', 'appConfig', 'services/AuthAPI', - 'utils/BemHelper', + 'utils/BemHelper', 'atlas-state', 'services/MomentAPI', 'less!welcome.less' @@ -43,7 +43,7 @@ define([ }); self.tokenExpired = authApi.tokenExpired; self.isLoggedIn = authApi.isAuthenticated; - self.isPermittedRunAs = ko.computed(() => self.isLoggedIn() && authApi.isPermittedRunAs()); + self.isPermittedRunAs = ko.computed(() => self.isLoggedIn() && authApi.isPermittedRunAs()); self.runAsLogin = ko.observable(); self.isGoogleIapAuth = ko.computed(() => authApi.authProvider() === authApi.AUTH_PROVIDERS.IAP); self.status = ko.computed(function () { @@ -61,11 +61,17 @@ define([ return 'Not logged in'; }); self.authProviders = appConfig.authProviders; + self.loginPlaceholder = ko.observable(); + self.passwordPlaceholder = ko.observable(); self.getAuthProvider = name => self.authProviders.filter(ap => ap.name === name)[0]; - self.toggleCredentialsForm =function () { + self.toggleCredentialsForm = function (provider) { self.isDbLoginAtt(!self.isDbLoginAtt()); + if (self.isDbLoginAtt()) { + self.loginPlaceholder(provider ? provider.loginPlaceholder : null); + self.passwordPlaceholder(provider ? provider.passwordPlaceholder : null); + } }; self.getAuthorizationHeader = function() { @@ -105,31 +111,34 @@ define([ }; self.signin = function (name) { - if(self.getAuthProvider(name).isUseCredentialsForm){ - self.authUrl(self.getAuthProvider(name).url); - self.toggleCredentialsForm(); - } - else { - var authProvider = self.getAuthProvider(name); - var loginUrl = self.serviceUrl + authProvider.url; - - if (authProvider.ajax == true) { - self.isInProgress(true); - $.ajax({ - url: loginUrl, - xhrFields: { - withCredentials: true - }, - success: self.onLoginSuccessful, - error: (jqXHR, textStatus, errorThrown) => self.onLoginFailed(jqXHR, ko.i18n('components.welcome.messages.loginFailed', 'Login failed')()), - }); + const selectedProvider = self.getAuthProvider(name); + if (selectedProvider.isUseCredentialsForm) { + self.authUrl(selectedProvider.url); + self.toggleCredentialsForm(selectedProvider); } else { - const parts = window.location.href.split('#'); - document.location = parts.length === 2 ? loginUrl + '?redirectUrl=' + parts[1] : loginUrl; + const loginUrl = self.serviceUrl + selectedProvider.url; + + if (selectedProvider.ajax == true) { + self.isInProgress(true); + $.ajax({ + url: loginUrl, + xhrFields: { + withCredentials: true + }, + success: self.onLoginSuccessful, + error: (jqXHR, textStatus, errorThrown) => self.onLoginFailed(jqXHR, ko.i18n('components.welcome.messages.loginFailed', 'Login failed')()), + }); + } else { + const parts = window.location.href.split('#'); + document.location = parts.length === 2 ? loginUrl + '?redirectUrl=' + parts[1] : loginUrl; + } } - } - }; + }; + if (self.authProviders.length === 1 && !self.isLoggedIn()) { + self.signin(self.authProviders[0].name); + } + self.signout = function () { self.isInProgress(true); if (authApi.authClient() === authApi.AUTH_CLIENTS.SAML) { diff --git a/js/config/app.js b/js/config/app.js index 0c17a970e..975941ed6 100644 --- a/js/config/app.js +++ b/js/config/app.js @@ -1,23 +1,30 @@ define(function () { - var appConfig = {}; + var appConfig = {}; - // default configuration - appConfig.api = { - name: 'Local', - url: 'http://localhost:8080/WebAPI/' + // default configuration + appConfig.api = { + name: 'Local', + url: 'http://localhost:8080/WebAPI/' }; + appConfig.disableBrowserCheck = false; // browser check will happen by default + appConfig.enablePermissionManagement = true; // allow UI to assign read/write permissions to entities appConfig.cacheSources = false; appConfig.pollInterval = 60000; - appConfig.cohortComparisonResultsEnabled = false; - appConfig.userAuthenticationEnabled = false; - appConfig.plpResultsEnabled = false; - appConfig.useExecutionEngine = false; - appConfig.viewProfileDates = false; + appConfig.cohortComparisonResultsEnabled = false; + appConfig.userAuthenticationEnabled = false; + appConfig.enableSkipLogin = false; // automatically opens login window when user is not authenticated + appConfig.plpResultsEnabled = false; + appConfig.useExecutionEngine = false; + appConfig.viewProfileDates = false; appConfig.enableCosts = false; - appConfig.supportUrl = "https://github.com/ohdsi/atlas/issues"; - appConfig.supportMail = "atlasadmin@your.org"; - appConfig.defaultLocale = "en"; - appConfig.authProviders = [ + appConfig.supportUrl = "https://github.com/ohdsi/atlas/issues"; + appConfig.supportMail = "atlasadmin@your.org"; + appConfig.feedbackContacts = 'For access or questions concerning the Atlas application please contact:'; + appConfig.feedbackCustomHtmlTemplate = ''; + appConfig.companyInfoCustomHtmlTemplate = ''; + appConfig.showCompanyInfo = true; + appConfig.defaultLocale = "en"; + appConfig.authProviders = [ { "name": "Windows", "url": "user/login/windows", @@ -59,7 +66,7 @@ define(function () { "url": "user/login/db", "ajax": true, "icon": "fa fa-database", - "isUseCredentialsForm":true + "isUseCredentialsForm": true }, { "name": "LDAP", @@ -88,7 +95,7 @@ define(function () { appConfig.xssOptions = { "whiteList": { "a": ["href", "class", "data-bind", "data-toggle", "aria-expanded"], - "button": ["class", "type", "data-toggle", "aria-expanded"], + "button": ["class", "type", "data-toggle", "aria-expanded"], "span": ["class", "data-bind"], "i": ["class", "id", "aria-hidden"], "div": ["class", "style", "id"], @@ -104,7 +111,7 @@ define(function () { "stripIgnoreTagBody": ['script'], }; appConfig.cemOptions = { - "evidenceLinkoutSources": ["medline_winnenburg","splicer"], + "evidenceLinkoutSources": ["medline_winnenburg", "splicer"], "sourceRestEndpoints": { "medline_winnenburg": "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&id={@ids}&retmode=json&tool=ohdsi_atlas&email=admin@ohdsi.org", }, @@ -114,47 +121,49 @@ define(function () { }, }; appConfig.enableTermsAndConditions = true; - appConfig.webAPIRoot = appConfig.api.url; - // todo: move "userAuthenticationEnabled", "plpResultsEnabled", etc into the object - appConfig.features = { - locationDistance: false, - }; + appConfig.webAPIRoot = appConfig.api.url; + // todo: move "userAuthenticationEnabled", "plpResultsEnabled", etc into the object + appConfig.features = { + locationDistance: false, + }; - appConfig.externalLibraries = []; + appConfig.externalLibraries = []; - appConfig.commonDataTableOptions = { - pageLength: { - ONLY_5: 5, - XS: 5, - S: 10, - M: 25, - L: 50, - }, - lengthMenu: { - ONLY_5: [[5], ['5']], - XS: [ - [5, 10], - ['5', '10'], - ], - S: [ + appConfig.commonDataTableOptions = { + pageLength: { + ONLY_5: 5, + XS: 5, + S: 10, + M: 25, + L: 50, + }, + lengthMenu: { + ONLY_5: [[5], ['5']], + XS: [ + [5, 10], + ['5', '10'], + ], + S: [ [10, 15, 20, 25, 50, -1], ['10', '15', '20', '25', '50', 'All'], - ], - M: [ + ], + M: [ [10, 25, 50, 100, -1], ['10', '25', '50', '100', 'All'], - ], - L: [ + ], + L: [ [25, 50, 75, 100, -1], ['25', '50', '75', '100', 'All'], - ], - } - }; + ], + } + }; + + appConfig.enablePersonCount = true; - appConfig.enablePersonCount = true; + // "Tagging" section is hidden by default + appConfig.enableTaggingSection = false; - // "Tagging" section is hidden by default - appConfig.enableTaggingSection = false; + appConfig.refreshTokenThreshold = 1000 * 60 * 60 * 4; // refresh auth token if it will expire within 4 hours - return appConfig; + return appConfig; }); diff --git a/js/config/terms-and-conditions-content-en.html b/js/config/terms-and-conditions-content-en.html index c6b5ffe60..0022d90cd 100644 --- a/js/config/terms-and-conditions-content-en.html +++ b/js/config/terms-and-conditions-content-en.html @@ -23,7 +23,7 @@

SNOMED INTERNATIONAL SNOMED CT LICENSE AGREEMENT

Territory”, “SNOMED CT” and “SNOMED CT Content” are as defined in the SNOMED International Affiliate License Agreement (see on the SNOMED International web siteon the SNOMED International website).
  • @@ -35,7 +35,7 @@

    SNOMED INTERNATIONAL SNOMED CT LICENSE AGREEMENT

    mlds.ihtsdotools.org, subject to acceptance of the Affiliate License Agreement (see on the SNOMED International web site)on the SNOMED International website).
  • @@ -44,7 +44,7 @@

    SNOMED INTERNATIONAL SNOMED CT LICENSE AGREEMENT

    not included in that list are "Non-Member Territories"€.
  • - End Users, that do not hold an SNOMED International Affiliate License, may + End Users, that do not hold the SNOMED International Affiliate License, may access SNOMED CT® using SNOMED International SNOMED CT Browser subject to acceptance of and adherence to the following sub-license limitations: diff --git a/js/const.js b/js/const.js index 7e4348e90..9bb28330d 100644 --- a/js/const.js +++ b/js/const.js @@ -159,6 +159,26 @@ define([ MomentApi.formatDateTimeWithFormat(d['VALID_END_DATE'], MomentApi.DATE_FORMAT), visible: false }, + { + title: ko.i18n('columns.rc', 'RC'), + data: 'RECORD_COUNT', + className: 'numeric' + }, + { + title: ko.i18n('columns.drc', 'DRC'), + data: 'DESCENDANT_RECORD_COUNT', + className: 'numeric' + }, + { + title: ko.i18n('columns.pc', 'PC'), + data: 'PERSON_COUNT', + className: 'numeric', + }, + { + title: ko.i18n('columns.dpc', 'DPC'), + data: 'DESCENDANT_PERSON_COUNT', + className: 'numeric', + }, { title: ko.i18n('columns.domain', 'Domain'), data: 'DOMAIN_ID' @@ -272,7 +292,7 @@ define([ SNOWFLAKE: { title: "Snowflake", dialect: "snowflake", - }, + }, SYNAPSE: { title: "Azure Synapse", dialect: "synapse", diff --git a/js/extensions/bindings/expressionCartoonBinding.js b/js/extensions/bindings/expressionCartoonBinding.js index fcccd540e..91e671935 100644 --- a/js/extensions/bindings/expressionCartoonBinding.js +++ b/js/extensions/bindings/expressionCartoonBinding.js @@ -695,9 +695,20 @@ define(['knockout', 'd3', 'd3-tip', 'lodash'], //cohdef.selectedCriteria(getCrit("wrapper",crit)); var evt = d3.event; var tt = $('div#cartoon-tooltip > div#tooltip'); + + const xTooltip = document.documentElement.clientWidth - 25 > evt.pageX + tt.width(); + const left = xTooltip ? + evt.pageX - tt.parent().offset().left + 10 : + evt.pageX - tt.parent().offset().left - tt.width() - 10; + + const yTooltip = document.documentElement.clientHeight - 25 > evt.pageY + tt.height(); + const top = yTooltip ? + evt.pageY - tt.parent().offset().top + 10 : + evt.pageY - tt.parent().offset().top - tt.height() - 10; + tt.css('display', 'inline') - .css('left', evt.pageX - tt.parent().offset().left) - .css('top', evt.pageY - tt.parent().offset().top) + .css('left', left) + .css('top', top) //console.log(`client: ${evt.clientX},${evt.clientY}, parent: ${JSON.stringify(tt.parent().offset())}`); }) .on("mouseout", function (crit) { diff --git a/js/extensions/bindings/i18nBinding.js b/js/extensions/bindings/i18nBinding.js index 81d1bed53..5bcbed48b 100644 --- a/js/extensions/bindings/i18nBinding.js +++ b/js/extensions/bindings/i18nBinding.js @@ -17,14 +17,14 @@ define(['knockout', 'atlas-state', 'lodash'], let options; if (arg2 === undefined) { defaultValue = undefined; - options = arg1; + options =arg1; } else { defaultValue = arg1; options = arg2; } return ko.pureComputed(() => { const tmpl = ko.i18n(key, defaultValue); - const unwrappedOptions = mapValues(options, ko.unwrap); + const unwrappedOptions = mapValues(options, ko.toJS); const compiledTemplate = template(ko.unwrap(tmpl), {sourceURL: 'i18n/templates[' + key + ']'}); return compiledTemplate(unwrappedOptions); }); diff --git a/js/main.js b/js/main.js index fda524e13..89c7fa77c 100644 --- a/js/main.js +++ b/js/main.js @@ -16,6 +16,7 @@ require(["./settings"], (settings) => { require([ 'bootstrap', 'ko.sortable', + 'databindings', 'services/PluginRegistry', ...Object.values(settings.cssPaths), ], function () { // bootstrap must come first diff --git a/js/pages/Route.js b/js/pages/Route.js index ed5c5cd9d..b4bc0f608 100644 --- a/js/pages/Route.js +++ b/js/pages/Route.js @@ -11,7 +11,7 @@ define([ checkPermission() { if (authApi.authProvider() === authApi.AUTH_PROVIDERS.IAP) { return authApi.loadUserInfo(); - } else if (appConfig.userAuthenticationEnabled && authApi.token() != null && authApi.tokenExpirationDate() > new Date()) { + } else if (appConfig.userAuthenticationEnabled && authApi.token() != null && this.timeToExpire() < appConfig.refreshTokenThreshold) { return authApi.refreshToken(); } return Promise.resolve(); @@ -25,6 +25,10 @@ define([ handler() { throw new Exception('Handler should be overriden'); } + + timeToExpire() { + return authApi.tokenExpirationDate() - new Date(); + } } class AuthorizedRoute extends Route { diff --git a/js/pages/characterizations/components/characterizations/characterization-view-edit.html b/js/pages/characterizations/components/characterizations/characterization-view-edit.html index a7ab7c349..b670757c5 100644 --- a/js/pages/characterizations/components/characterizations/characterization-view-edit.html +++ b/js/pages/characterizations/components/characterizations/characterization-view-edit.html @@ -20,10 +20,14 @@ + + - + + +
  • @@ -103,4 +107,4 @@ loadAvailableTagsFn: $component.loadAvailableTags, checkAssignPermissionFn: $component.checkAssignPermission, checkUnassignPermissionFn: $component.checkUnassignPermission -"> \ No newline at end of file +"> diff --git a/js/pages/characterizations/components/characterizations/characterization-view-edit.js b/js/pages/characterizations/components/characterizations/characterization-view-edit.js index c7042f914..2cf3cdbb8 100644 --- a/js/pages/characterizations/components/characterizations/characterization-view-edit.js +++ b/js/pages/characterizations/components/characterizations/characterization-view-edit.js @@ -70,7 +70,7 @@ define([ this.selectedSourceId = ko.observable(params.router.routerParams().sourceId); this.areStratasNamesEmpty = ko.observable(); this.duplicatedStrataNames = ko.observable([]); - + this.enablePermissionManagement = config.enablePermissionManagement; this.designDirtyFlag = sharedState.CohortCharacterization.dirtyFlag; this.loading = ko.observable(false); this.defaultName = ko.unwrap(constants.newEntityNames.characterization); diff --git a/js/pages/characterizations/components/characterizations/characterization-view-edit/characterization-results.html b/js/pages/characterizations/components/characterizations/characterization-view-edit/characterization-results.html index f5a79779a..5ba3b0059 100644 --- a/js/pages/characterizations/components/characterizations/characterization-view-edit/characterization-results.html +++ b/js/pages/characterizations/components/characterizations/characterization-view-edit/characterization-results.html @@ -146,6 +146,7 @@

    +
    }" />
    +
    + + +
    +
    +
    + +
    +
    +
    +

    Cohort Legend

    +
    +

    + + + + +
    +
    +
    +
    diff --git a/js/pages/characterizations/components/characterizations/characterization-view-edit/characterization-results.js b/js/pages/characterizations/components/characterizations/characterization-view-edit/characterization-results.js index c4aba324e..7864398bf 100644 --- a/js/pages/characterizations/components/characterizations/characterization-view-edit/characterization-results.js +++ b/js/pages/characterizations/components/characterizations/characterization-view-edit/characterization-results.js @@ -32,6 +32,7 @@ define([ 'components/visualizations/line-chart', 'components/charts/scatterplot', 'components/charts/splitBoxplot', + 'components/charts/horizontalBoxplot', 'd3-scale-chromatic', ], function ( ko, @@ -470,23 +471,44 @@ define([ })); } + getBoxplotStruct(cohort, stat) { + return { + Category: cohort.cohortName, + min: stat.min[0][cohort.cohortId], + max: stat.max[0][cohort.cohortId], + median: stat.median[0][cohort.cohortId], + LIF: stat.p10[0][cohort.cohortId], + q1: stat.p25[0][cohort.cohortId], + q3: stat.p75[0][cohort.cohortId], + UIF: stat.p90[0][cohort.cohortId] + }; + } + convertBoxplotData(analysis) { + return [{ + target: this.getBoxplotStruct(analysis.cohorts[0], analysis.data[0]), + compare: this.getBoxplotStruct(analysis.cohorts[1], analysis.data[0]), + }]; + } - const getBoxplotStruct = (cohort, stat) => ({ - Category: cohort.cohortName, - min: stat.min[0][cohort.cohortId], - max: stat.max[0][cohort.cohortId], - median: stat.median[0][cohort.cohortId], - LIF: stat.p10[0][cohort.cohortId], - q1: stat.p25[0][cohort.cohortId], - q3: stat.p75[0][cohort.cohortId], - UIF: stat.p90[0][cohort.cohortId] + convertHorizontalBoxplotData(analysis) { + return analysis.cohorts.map(cohort => { + return this.getBoxplotStruct(cohort, analysis.data[0]); }); + } - return [{ - target: getBoxplotStruct(analysis.cohorts[0], analysis.data[0]), - compare: getBoxplotStruct(analysis.cohorts[1], analysis.data[0]), - }] + prepareLegendBoxplotData (analysis) { + const cohortNames = analysis.cohorts.map(d => d.cohortName); + const legendColorsSchema = d3.scaleOrdinal().domain(cohortNames) + .range(utils.colorHorizontalBoxplot); + + const legendColors = cohortNames.map(cohort => { + return { + cohortName: cohort, + cohortColor: legendColorsSchema(cohort) + }; + }); + return legendColors.reverse(); } analysisTitle(data) { diff --git a/js/pages/characterizations/components/characterizations/characterization-view-edit/characterization-results.less b/js/pages/characterizations/components/characterizations/characterization-view-edit/characterization-results.less index ee0335ec3..bf0a5c190 100644 --- a/js/pages/characterizations/components/characterizations/characterization-view-edit/characterization-results.less +++ b/js/pages/characterizations/components/characterizations/characterization-view-edit/characterization-results.less @@ -142,6 +142,8 @@ &__analysis-results { align-items: center; display: flex; + flex-wrap: wrap; + width: 100%; } &__table-wrapper, &__chart-wrapper { @@ -150,8 +152,8 @@ } &__table-wrapper { - flex-grow: 65; - + flex: 2; + width:100%; .dt-buttons { padding-bottom: 1rem; padding-right: 1rem; @@ -159,7 +161,8 @@ } &__chart-wrapper { - flex-grow: 35; + flex: 1; + width: 100%; margin-left: 2rem; border: 1px solid #ccc; padding: 1.5rem 1.5rem 0.45rem 0; @@ -201,6 +204,21 @@ &__report-table { width: 100% !important; + height: fit-content; // needed if pct-cell has more than one-line height (to correctly fill it vertically) + + td.pct-cell { + padding: 0 !important; + height: 100%; + } + div.pct-fill { + background-color: #afd9ee; + height: 100%; + + div { + vertical-align: middle; + padding: 8px 10px; + } + } } &__greyed-row { @@ -233,4 +251,38 @@ &__action-ico { margin-left: 0.5rem; } +} + +.characterization-results-boxplot-container { + display: flex; + width: 50%; +} + +.characterization-results-legend-container { + min-width: 300px; + flex: 0.5; + margin-left: 16px; +} + +.characterization-results-container { + display: flex; + width: 100%; +} +.legend-header { + margin-top: 0; +} +.swatch { + width: 16px; + height: 16px; + margin: 6px 0; +} + +.color-cell { + width: 24px; + vertical-align: baseline; +} + +.legend-cohort-name { + font-size: 1rem; + font-weight: bold; } \ No newline at end of file diff --git a/js/pages/characterizations/components/characterizations/characterization-view-edit/explore-prevalence.js b/js/pages/characterizations/components/characterizations/characterization-view-edit/explore-prevalence.js index ff56f9839..befd46c7c 100644 --- a/js/pages/characterizations/components/characterizations/characterization-view-edit/explore-prevalence.js +++ b/js/pages/characterizations/components/characterizations/characterization-view-edit/explore-prevalence.js @@ -76,7 +76,10 @@ define([ return { title: ko.i18n('columns.pct', 'Pct'), class: this.classes('col-pct'), - render: (s, p, d) => utils.formatPct(d.pct[strata] || 0), + render: (s, p, d) => { + const pct = utils.formatPct(d.pct[strata] || 0); + return `
    ${pct}
    `; + }, }; } diff --git a/js/pages/characterizations/components/characterizations/characterization-view-edit/explore-prevalence.less b/js/pages/characterizations/components/characterizations/characterization-view-edit/explore-prevalence.less index 5b991fa6e..9853d2f11 100644 --- a/js/pages/characterizations/components/characterizations/characterization-view-edit/explore-prevalence.less +++ b/js/pages/characterizations/components/characterizations/characterization-view-edit/explore-prevalence.less @@ -35,9 +35,23 @@ &__col-pct { width: 4rem; } + td&__col-pct { + padding: 0 !important; + height: 100%; + } - .dt-buttons { - padding-bottom: 1rem; - padding-right: 1rem; - } + .dt-buttons { + padding-bottom: 1rem; + padding-right: 1rem; + } + + div.pct-fill { + background-color: #afd9ee; + height: 100%; + + div { + vertical-align: middle; + padding: 8px 10px; + } + } } \ No newline at end of file diff --git a/js/pages/characterizations/components/characterizations/characterization-view-edit/utils.js b/js/pages/characterizations/components/characterizations/characterization-view-edit/utils.js index ec2b53c5c..1d499cde1 100644 --- a/js/pages/characterizations/components/characterizations/characterization-view-edit/utils.js +++ b/js/pages/characterizations/components/characterizations/characterization-view-edit/utils.js @@ -6,8 +6,25 @@ define([ 'numeral' ], function( const formatPct = (val) => numeral(val).format('0.00') + '%'; + const colorHorizontalBoxplot = [ + "#ff9315", + "#0d61ff", + "gold", + "blue", + "green", + "red", + "black", + "orange", + "brown", + "grey", + "slateblue", + "grey1", + "darkgreen" + ]; + return { formatPct, formatStdDiff, + colorHorizontalBoxplot }; }); \ No newline at end of file diff --git a/js/pages/characterizations/components/feature-analyses/feature-analysis-view-edit.html b/js/pages/characterizations/components/feature-analyses/feature-analysis-view-edit.html index 3ec6c84c7..85d579285 100644 --- a/js/pages/characterizations/components/feature-analyses/feature-analysis-view-edit.html +++ b/js/pages/characterizations/components/feature-analyses/feature-analysis-view-edit.html @@ -17,9 +17,13 @@ + + + +
    @@ -53,4 +57,4 @@ grantAccessFn: $component.grantAccess, revokeAccessFn: $component.revokeAccess, loadRoleSuggestionsFn: $component.loadAccessRoleSuggestions -"> \ No newline at end of file +"> diff --git a/js/pages/characterizations/components/feature-analyses/feature-analysis-view-edit.js b/js/pages/characterizations/components/feature-analyses/feature-analysis-view-edit.js index 01679cf52..84943e54c 100644 --- a/js/pages/characterizations/components/feature-analyses/feature-analysis-view-edit.js +++ b/js/pages/characterizations/components/feature-analyses/feature-analysis-view-edit.js @@ -152,7 +152,7 @@ define([ return !this.isNewEntity() && this.initialFeatureType() === featureTypes.PRESET; }); this.editorClasses = ko.computed(() => this.classes({ element: 'content', modifiers: this.canEdit() ? '' : 'disabled' })) - + this.enablePermissionManagement = config.enablePermissionManagement; this.selectedTabKey = ko.observable(); this.componentParams = ko.observable({ ...params, diff --git a/js/pages/characterizations/services/CharacterizationService.js b/js/pages/characterizations/services/CharacterizationService.js index 3cf96f88b..3378e63ee 100644 --- a/js/pages/characterizations/services/CharacterizationService.js +++ b/js/pages/characterizations/services/CharacterizationService.js @@ -3,11 +3,13 @@ define([ 'services/file', 'appConfig', 'utils/ExecutionUtils', + 'services/AuthAPI', ], function ( httpService, fileService, config, executionUtils, + authApi ) { function loadCharacterizationList() { return httpService @@ -21,10 +23,13 @@ define([ .then(res => res.data); } - function loadCharacterizationDesign(id) { - return httpService + async function loadCharacterizationDesign(id) { + const result = await httpService .doGet(config.webAPIRoot + 'cohort-characterization/' + id + '/design') .then(res => res.data); + await authApi.refreshToken(); + return result; + } function loadCharacterizationExportDesign(id) { @@ -33,12 +38,12 @@ define([ .then(res => res.data); } - function createCharacterization(design) { - return httpService.doPost(config.webAPIRoot + 'cohort-characterization', design).then(res => res.data); + async function createCharacterization(design) { + return authApi.executeWithRefresh(httpService.doPost(config.webAPIRoot + 'cohort-characterization', design).then(res => res.data)); } - function copyCharacterization(id) { - return httpService.doPost(config.webAPIRoot + 'cohort-characterization/' + id).then(res => res.data); + async function copyCharacterization(id) { + return authApi.executeWithRefresh(httpService.doPost(config.webAPIRoot + 'cohort-characterization/' + id).then(res => res.data)); } function updateCharacterization(id, design) { @@ -75,16 +80,16 @@ define([ .then(res => res.data); } - function generate(ccId, sourcekey) { - return httpService + async function generate(ccId, sourcekey) { + return authApi.executeWithRefresh(httpService .doPost(config.webAPIRoot + 'cohort-characterization/' + ccId + '/generation/' + sourcekey) - .then(res => res.data); + .then(res => res.data)); } - function importCharacterization(design) { - return httpService + async function importCharacterization(design) { + return authApi.executeWithRefresh(httpService .doPost(config.webAPIRoot + 'cohort-characterization/import', design) - .then(res => res.data); + .then(res => res.data)); } function getPrevalenceStatsByGeneration(generationId, analysisId, cohortId, covariateId) { @@ -106,7 +111,8 @@ define([ } function exportConceptSets(id) { - return fileService.loadZip(`${config.webAPIRoot}cohort-characterization/${id}/export/conceptset`); + return fileService.loadZip(`${config.webAPIRoot}cohort-characterization/${id}/export/conceptset`, + `cohort_characterization_${id}_export.zip`); } function runDiagnostics(design) { return httpService @@ -124,9 +130,9 @@ define([ .then(res => res.data); } - function copyVersion(id, versionNumber) { - return httpService.doPut(`${config.webAPIRoot}cohort-characterization/${id}/version/${versionNumber}/createAsset`) - .then(res => res.data); + async function copyVersion(id, versionNumber) { + return authApi.executeWithRefresh(httpService.doPut(`${config.webAPIRoot}cohort-characterization/${id}/version/${versionNumber}/createAsset`) + .then(res => res.data)); } function updateVersion(version) { diff --git a/js/pages/characterizations/services/FeatureAnalysisService.js b/js/pages/characterizations/services/FeatureAnalysisService.js index fa47aec0c..aba64ef63 100644 --- a/js/pages/characterizations/services/FeatureAnalysisService.js +++ b/js/pages/characterizations/services/FeatureAnalysisService.js @@ -2,26 +2,28 @@ define([ 'services/http', 'services/file', 'appConfig', + 'services/AuthAPI', ], function ( httpService, fileService, - config + config, + authApi ) { function loadFeatureAnalysisList() { return httpService.doGet(config.webAPIRoot + 'feature-analysis?size=100000').then(res => res.data); } - function loadFeatureAnalysis(id) { - return httpService.doGet(config.webAPIRoot + `feature-analysis/${id}`).then(res => res.data); + async function loadFeatureAnalysis(id) { + return authApi.executeWithRefresh(httpService.doGet(config.webAPIRoot + `feature-analysis/${id}`).then(res => res.data)); } function loadFeatureAnalysisDomains() { return httpService.doGet(config.webAPIRoot + 'feature-analysis/domains').then(res => res.data); } - function createFeatureAnalysis(design) { - return request = httpService.doPost(config.webAPIRoot + 'feature-analysis', design).then(res => res.data); + async function createFeatureAnalysis(design) { + return authApi.executeWithRefresh(httpService.doPost(config.webAPIRoot + 'feature-analysis', design).then(res => res.data)); } function updateFeatureAnalysis(id, design) { @@ -45,8 +47,8 @@ define([ return httpService.doGet(`${config.webAPIRoot}feature-analysis/aggregates`).then(res => res.data); } - function copyFeatureAnalysis(id) { - return httpService.doGet(`${config.webAPIRoot}feature-analysis/${id}/copy`); + async function copyFeatureAnalysis(id) { + return authApi.executeWithRefresh(httpService.doGet(`${config.webAPIRoot}feature-analysis/${id}/copy`)); } return { diff --git a/js/pages/characterizations/services/conversion/BaseStatConverter.js b/js/pages/characterizations/services/conversion/BaseStatConverter.js index e1c8ddea1..f48e209fa 100644 --- a/js/pages/characterizations/services/conversion/BaseStatConverter.js +++ b/js/pages/characterizations/services/conversion/BaseStatConverter.js @@ -136,11 +136,15 @@ define([ getColumn(label, field, strata, cohortId, formatter) { return { title: label, + className: field === 'pct' ? 'pct-cell' : '', render: (s, p, d) => { let res = d[field][strata] && d[field][strata][cohortId] || 0; if (p === "display" && formatter) { res = formatter(res); } + if (field === 'pct') { + return `
    ${res}
    `; + } return res; } }; diff --git a/js/pages/cohort-definitions/cohort-definition-manager.css b/js/pages/cohort-definitions/cohort-definition-manager.css index db5777dc3..838705a78 100644 --- a/js/pages/cohort-definitions/cohort-definition-manager.css +++ b/js/pages/cohort-definitions/cohort-definition-manager.css @@ -87,4 +87,28 @@ color: #265a88; cursor: pointer; } + +.generation-container { + margin: 0 6px; +} +.generation-heading { + font-size: 1.8rem; + color: #333; + font-weight: 500; + margin: 12px 0 3px 0; +} +.only-results-checkbox { +} +table.sources-table td.generation-buttons-column { + text-align: right; +} +table.sources-table thead th, table.sources-table tbody tr:first-child td { + padding: 4px 10px; +} +table.sources-table tbody td { + padding: 0 10px 4px; +} +.generation-buttons { + white-space: nowrap; +} \ No newline at end of file diff --git a/js/pages/cohort-definitions/cohort-definition-manager.html b/js/pages/cohort-definitions/cohort-definition-manager.html index 7a4bdff68..2665a0ca7 100644 --- a/js/pages/cohort-definitions/cohort-definition-manager.html +++ b/js/pages/cohort-definitions/cohort-definition-manager.html @@ -31,12 +31,15 @@ data-bind="visible: !previewVersion(), title: ko.i18n('cohortDefinitions.cohortDefinitionManager.createCopyCohortTitle', 'Create a copy of this cohort definition'), click: copy, enable: canCopy() && !isProcessing()"> + data-bind="visible: !previewVersion(), title: ko.i18n('cohortDefinitions.cohortDefinitionManager.getLinkCohortTitle', 'Get a link to this cohort definition'), enable: !dirtyFlag().isDirty() && !isProcessing(), click: function () { $component.cohortLinkModalOpened(true) }"> + + + +
    @@ -116,7 +116,7 @@ diff --git a/js/pages/concept-sets/components/concept/components/tabs/concept-hierarchy.js b/js/pages/concept-sets/components/concept/components/tabs/concept-hierarchy.js index 5c1e17fbf..af2c0f483 100644 --- a/js/pages/concept-sets/components/concept/components/tabs/concept-hierarchy.js +++ b/js/pages/concept-sets/components/concept/components/tabs/concept-hierarchy.js @@ -31,7 +31,6 @@ define([ this.currentConceptId = params.currentConceptId; this.hasInfoAccess = params.hasInfoAccess; this.isAuthenticated = params.isAuthenticated; - this.addConcepts = params.addConcepts; this.tableOptions = commonUtils.getTableOptions('M'); this.hierarchyPillMode = ko.observable('all'); this.relatedConcepts = ko.observableArray([]); @@ -128,6 +127,10 @@ define([ this.loadHierarchyConcepts(); } + getSelectedConcepts(concepts) { + return commonUtils.getSelectedConcepts(concepts); + } + hasRelationship(concept, relationships) { for (var r = 0; r < concept.RELATIONSHIPS.length; r++) { for (var i = 0; i < relationships.length; i++) { diff --git a/js/pages/concept-sets/components/concept/components/tabs/concept-related.html b/js/pages/concept-sets/components/concept/components/tabs/concept-related.html index 8afb0c7c7..06cec7989 100644 --- a/js/pages/concept-sets/components/concept/components/tabs/concept-related.html +++ b/js/pages/concept-sets/components/concept/components/tabs/concept-related.html @@ -13,7 +13,7 @@ diff --git a/js/pages/concept-sets/components/concept/components/tabs/concept-related.js b/js/pages/concept-sets/components/concept/components/tabs/concept-related.js index aa8101adf..d2848fb9f 100644 --- a/js/pages/concept-sets/components/concept/components/tabs/concept-related.js +++ b/js/pages/concept-sets/components/concept/components/tabs/concept-related.js @@ -153,6 +153,10 @@ define([ this.loadRelatedConcepts(); } + getSelectedConcepts(concepts) { + return commonUtils.getSelectedConcepts(concepts); + } + enhanceConcept(concept) { return { ...concept, diff --git a/js/pages/concept-sets/components/concept/concept-manager.html b/js/pages/concept-sets/components/concept/concept-manager.html index 1b3b9c1b4..4c69925c1 100644 --- a/js/pages/concept-sets/components/concept/concept-manager.html +++ b/js/pages/concept-sets/components/concept/concept-manager.html @@ -34,6 +34,12 @@ componentName: 'concept-count', componentParams: $component.tabParams }, + { + title: ko.i18n('cs.manager.concept.tabs.drilldownReport.caption', 'Drilldown Report'), + key: 'report', + componentName: 'concept-drilldown-report', + componentParams: $component.tabParams + } ]"> \ No newline at end of file diff --git a/js/pages/concept-sets/components/concept/concept-manager.js b/js/pages/concept-sets/components/concept/concept-manager.js index d50c643cf..c0be44453 100644 --- a/js/pages/concept-sets/components/concept/concept-manager.js +++ b/js/pages/concept-sets/components/concept/concept-manager.js @@ -7,14 +7,13 @@ define([ 'utils/CommonUtils', 'services/ConceptSet', 'components/conceptset/ConceptSetStore', - 'components/conceptset/utils', + 'components/conceptset/utils', 'utils/Renderers', 'atlas-state', 'services/http', '../../const', 'services/AuthAPI', '../../PermissionService', - 'const', 'faceted-datatable', 'components/heading', 'components/conceptLegend/concept-legend', @@ -25,6 +24,7 @@ define([ './components/tabs/concept-related', './components/tabs/concept-hierarchy', './components/tabs/concept-count', + './components/tabs/concept-drilldown-report', ], function ( ko, view, @@ -41,7 +41,6 @@ define([ constants, authApi, PermissionService, - globalConstants, ) { class ConceptManager extends AutoBind(Page) { constructor(params) { diff --git a/js/pages/concept-sets/components/tabs/conceptset-compare.html b/js/pages/concept-sets/components/tabs/conceptset-compare.html index 4e13bda53..21fab5f18 100644 --- a/js/pages/concept-sets/components/tabs/conceptset-compare.html +++ b/js/pages/concept-sets/components/tabs/conceptset-compare.html @@ -9,6 +9,8 @@ RC DRC + PC + DPC @@ -27,6 +29,10 @@ +
    + + +
    +
    + + +
    @@ -99,10 +101,10 @@ class="btn btn-success btn-sm" data-bind=" click: navigateToSearchPage, - css: { disabled: !canEditCurrentConceptSet() }, text: ko.i18n('components.conceptSet.addConcepts', 'Add concepts') " > + \ No newline at end of file diff --git a/js/pages/concept-sets/components/tabs/coneptset-compare-const.js b/js/pages/concept-sets/components/tabs/coneptset-compare-const.js new file mode 100644 index 000000000..26368fbf5 --- /dev/null +++ b/js/pages/concept-sets/components/tabs/coneptset-compare-const.js @@ -0,0 +1,13 @@ +define([], function () { + + const expressionType = { + BRIEF: 'CONCEPT_NAME_CODE_AND_VOCABULARY_ID_ONLY', + FULL: 'FULL' + }; + const requiredHeader = ['concept_name', 'concept_name', 'vocabulary_id']; + + return { + requiredHeader, + expressionType + }; +}); \ No newline at end of file diff --git a/js/pages/concept-sets/conceptset-manager.html b/js/pages/concept-sets/conceptset-manager.html index d17e4d450..a1643e25a 100644 --- a/js/pages/concept-sets/conceptset-manager.html +++ b/js/pages/concept-sets/conceptset-manager.html @@ -26,9 +26,13 @@ + + + + diff --git a/js/pages/concept-sets/conceptset-manager.js b/js/pages/concept-sets/conceptset-manager.js index 650a3b5d5..7402046ed 100644 --- a/js/pages/concept-sets/conceptset-manager.js +++ b/js/pages/concept-sets/conceptset-manager.js @@ -3,8 +3,8 @@ define([ 'text!./conceptset-manager.html', 'pages/Page', 'utils/AutoBind', - 'utils/CommonUtils', - 'appConfig', + 'utils/CommonUtils', + 'appConfig', './const', 'const', 'components/conceptset/utils', @@ -46,11 +46,11 @@ define([ 'components/ac-access-denied', 'components/versions/versions' ], function ( - ko, + ko, view, Page, AutoBind, - commonUtils, + commonUtils, config, constants, globalConstants, @@ -75,7 +75,8 @@ define([ constructor(params) { super(params); this.commonUtils = commonUtils; - this.conceptSetStore = ConceptSetStore.repository(); + this.conceptSetStore = ConceptSetStore.repository(); + this.selectedSource = ko.observable(); this.currentConceptSet = ko.pureComputed(() => this.conceptSetStore.current()); this.previewVersion = sharedState.currentConceptSetPreviewVersion; this.currentConceptSetDirtyFlag = sharedState.RepositoryConceptSet.dirtyFlag; @@ -173,6 +174,7 @@ define([ this.canCopy = ko.computed(() => { return this.currentConceptSet() && this.currentConceptSet().id > 0; }); + this.enablePermissionManagement = config.enablePermissionManagement; this.isSaving = ko.observable(false); this.isDeleting = ko.observable(false); this.isOptimizing = ko.observable(false); @@ -237,6 +239,7 @@ define([ tableOptions, canEdit: this.canEdit, currentConceptSet: this.conceptSetStore.current, + selectedSource: this.selectedSource, conceptSetStore: this.conceptSetStore, loading: this.conceptSetStore.loadingIncluded }, @@ -251,6 +254,7 @@ define([ tableOptions, canEdit: this.canEdit, conceptSetStore: this.conceptSetStore, + selectedSource: this.selectedSource, loading: this.conceptSetStore.loadingSourceCodes }, }, @@ -263,6 +267,7 @@ define([ tableOptions, canEdit: this.canEdit, conceptSetStore: this.conceptSetStore, + selectedSource: this.selectedSource, loading: this.conceptSetStore.loadingRecommended } }, @@ -303,6 +308,7 @@ define([ componentParams: { ...params, saveConceptSetFn: this.saveConceptSet, + selectedSource: this.selectedSource, saveConceptSetShow: this.saveConceptSetShow, }, hidden: () => !!this.previewVersion() @@ -358,7 +364,7 @@ define([ } }); - this.conceptSetStore.isEditable(this.canEdit()); + this.conceptSetStore.isEditable(this.canEdit()); this.subscriptions.push(this.conceptSetStore.observer.subscribe(async () => { // when the conceptSetStore changes (either through a new concept set being loaded or changes to concept set options), the concept set resolves and the view is refreshed. // this must be done within the same subscription due to the asynchronous nature of the AJAX and UI interface (ie: user can switch tabs at any time) @@ -531,7 +537,7 @@ define([ this.currentConceptSet().name(responseWithName.copyName); this.currentConceptSet().id = 0; this.currentConceptSetDirtyFlag().reset(); - this.saveConceptSet(this.currentConceptSet(), "#txtConceptSetName"); + await this.saveConceptSet(this.currentConceptSet(), "#txtConceptSetName"); } async optimize() { diff --git a/js/pages/configuration/configuration.html b/js/pages/configuration/configuration.html index 1c8e7039e..48f12d7d8 100644 --- a/js/pages/configuration/configuration.html +++ b/js/pages/configuration/configuration.html @@ -137,6 +137,11 @@ +
    + + + +
    diff --git a/js/pages/configuration/configuration.js b/js/pages/configuration/configuration.js index c211e759f..1a95dd460 100644 --- a/js/pages/configuration/configuration.js +++ b/js/pages/configuration/configuration.js @@ -77,6 +77,7 @@ define([ }); this.canImport = ko.pureComputed(() => this.isAuthenticated() && authApi.isPermittedImportUsers()); + this.canManageTags = ko.pureComputed(() => this.isAuthenticated() && authApi.isPermittedTagsManagement()); this.canClearServerCache = ko.pureComputed(() => { return config.userAuthenticationEnabled && this.isAuthenticated() && authApi.isPermittedClearServerCache() }); @@ -86,7 +87,7 @@ define([ this.checkJobs(); this.checkReindexJob(); }, - interval: 5000 + interval: config.pollInterval }); this.searchAvailable = ko.observable(false); diff --git a/js/pages/configuration/routes.js b/js/pages/configuration/routes.js index 1e1b901a0..8c9d5fe97 100644 --- a/js/pages/configuration/routes.js +++ b/js/pages/configuration/routes.js @@ -52,6 +52,11 @@ define( router.setCurrentView('source-manager', { sourceId }); }); }), + '/tag-management': new AuthorizedRoute(() => { + require(['./tag-management/tag-management'], function () { + router.setCurrentView('tag-management'); + }); + }), }; } diff --git a/js/pages/configuration/tag-management/tag-management.html b/js/pages/configuration/tag-management/tag-management.html new file mode 100644 index 000000000..3b80756da --- /dev/null +++ b/js/pages/configuration/tag-management/tag-management.html @@ -0,0 +1,181 @@ + + + +
    + +

    + + + + + +
    + +

    + + + +
    + + + + + + + + +
    + + + +
    +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    +
    + + + + +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    + + + + +
    + + + +
    +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    +
    + + + + +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    \ No newline at end of file diff --git a/js/pages/configuration/tag-management/tag-management.js b/js/pages/configuration/tag-management/tag-management.js new file mode 100644 index 000000000..4be041747 --- /dev/null +++ b/js/pages/configuration/tag-management/tag-management.js @@ -0,0 +1,388 @@ +define([ + 'knockout', + 'text!./tag-management.html', + 'pages/Page', + 'utils/AutoBind', + 'utils/CommonUtils', + 'utils/DatatableUtils', + 'appConfig', + 'services/AuthAPI', + 'services/Tags', + 'atlas-state', + 'databindings', + 'components/ac-access-denied', + 'components/heading', + 'less!./tag-management.less', +], function ( + ko, + view, + Page, + AutoBind, + commonUtils, + datatableUtils, + config, + authApi, + TagsService +) { + const DEFAULT_TAG_COLOR = '#cecece'; + const DEFAULT_TAG_ICON = 'fa fa-tag'; + + class TagManagement extends AutoBind(Page) { + constructor(params) { + super(params); + this.loading = ko.observable(); + this.tableOptions = commonUtils.getTableOptions('S'); + this.isAuthenticated = authApi.isAuthenticated; + + this.hasAccess = ko.pureComputed(() => { + if (!config.userAuthenticationEnabled) { + return true; + } else { + return this.isAuthenticated() && authApi.isPermittedTagsManagement(); + } + }); + + this.allTags = ko.observableArray(); + + this.tagGroups = ko.observableArray(); + this.showTagsForGroup = ko.observable(); + this.showTagsForGroup.subscribe((group) => { + if (!group) { + this.tags([]); + } else { + this.tags(this.allTags().filter(t => t.groups && t.groups.length > 0 && t.groups[0].id === group.id)); + } + }); + this.showTagGroupModal = ko.observable(false); + this.currentTagGroup = ko.observable(); + + this.tags = ko.observableArray(); + this.showTagModal = ko.observable(false); + this.currentTag = ko.observable(); + + this.tagGroupColumns = [ + { + title: ko.i18n('columns.name', 'Name'), + width: '100px', + data: 'name' + }, + { + title: ko.i18n('configuration.tagManagement.color', 'Color'), + width: '38px', + sortable: false, + render: (s, p, d) => { + return `     `; + } + }, + { + title: ko.i18n('configuration.tagManagement.icon', 'Icon'), + width: '30px', + sortable: false, + render: (s, p, d) => { + return ``; + } + }, + { + title: ko.i18n('configuration.tagManagement.mandatory', 'Mandatory'), + width: '40px', + render: (s, p, d) => { + return d.mandatory ? `` : ''; + } + }, + { + title: ko.i18n('configuration.tagManagement.showInAssetsBrowser', 'Show Column'), + width: '40px', + render: (s, p, d) => { + return d.showGroup ? `` : ''; + } + }, + { + title: ko.i18n('configuration.tagManagement.allowMultiple', 'Multiple'), + width: '40px', + render: (s, p, d) => { + return d.multiSelection ? `` : ''; + } + }, + { + title: ko.i18n('configuration.tagManagement.allowCustom', 'Free‑form'), + width: '40px', + render: (s, p, d) => { + return d.allowCustom ? `` : ''; + } + }, + { + title: ko.i18n('columns.created', 'Created'), + width: '120px', + render: (s, p, d) => { + const dateTime = datatableUtils.getDateFieldFormatter('createdDate')(s, p, d); + return `${dateTime}`; + }, + }, + { + title: ko.i18n('columns.author', 'Author'), + width: '100px', + render: (s, p, d) => { + const author = datatableUtils.getCreatedByFormatter('System')(s, p, d); + return `${author}`; + }, + }, + { + title: ko.i18n('columns.description', 'Description'), + width: '225px', + render: (s, p, d) => { + const desc = d.description || '-'; + return `${desc}`; + } + }, + { + title: '', + width: '100px', + sortable: false, + render: (s, p, d) => { + if (this.showTagsForGroup() && this.showTagsForGroup().id === d.id) { + d.resetCurrentGroup = () => this.showTagsForGroup(null); + return ``; + } else { + d.setShowTagsForGroup = () => this.showTagsForGroup(d); + return ``; + } + } + }, + { + title: '', + width: '100px', + sortable: false, + render: (s, p, d) => { + d.editTagGroup = () => { + this.currentTagGroup({ + ...d, + name: ko.observable(d.name), + color: ko.observable(d.color), + icon: ko.observable(d.icon), + }); + this.showTagGroupModal(true); + }; + return ``; + } + }, + { + title: '', + width: '100px', + sortable: false, + render: (s, p, d) => { + d.deleteTag = () => this.deleteTag(d); + return ``; + } + } + ]; + + this.tagColumns = [ + { + title: ko.i18n('columns.name', 'Name'), + width: '100px', + render: (s, p, d) => { + return ` + + ${d.name.length > 22 ? d.name.substring(0, 20) + '...' : d.name} + `; + } + }, + { + title: ko.i18n('configuration.tagManagement.protected', 'Protected'), + width: '30px', + render: (s, p, d) => { + return d.permissionProtected ? `` : ''; + } + }, + { + title: ko.i18n('columns.created', 'Created'), + width: '120px', + render: (s, p, d) => { + const dateTime = datatableUtils.getDateFieldFormatter('createdDate')(s, p, d); + return `${dateTime}`; + }, + }, + { + title: ko.i18n('columns.author', 'Author'), + width: '100px', + render: (s, p, d) => { + const author = datatableUtils.getCreatedByFormatter('System')(s, p, d); + return `${author}`; + }, + }, + { + title: ko.i18n('columns.description', 'Description'), + width: '225px', + render: (s, p, d) => { + const desc = d.description || '-'; + return `${desc}`; + } + }, + { + title: ko.i18n('columns.usageCount', 'Usage count'), + width: '90px', + data: 'count' + }, + { + title: '', + width: '100px', + sortable: false, + render: (s, p, d) => { + d.editTag = () => { + this.currentTag({ + ...d, + name: ko.observable(d.name), + color: ko.observable(d.color), + icon: ko.observable(d.icon), + }); + this.showTagModal(true); + }; + return ``; + } + }, + { + title: '', + width: '100px', + sortable: false, + render: (s, p, d) => { + d.deleteTag = () => this.deleteTag(d); + return ``; + } + } + ]; + } + + async onPageCreated() { + const res = await TagsService.loadAvailableTags(); + this.allTags(res); + this.tagGroups(res.filter(t => !t.groups || t.groups.length === 0)); + } + + createGroup() { + this.currentTagGroup({ + groups: [], + name: ko.observable(''), + color: ko.observable(DEFAULT_TAG_COLOR), + icon: ko.observable(DEFAULT_TAG_ICON), + }); + this.showTagGroupModal(true); + } + + createTag() { + this.currentTag({ + name: ko.observable(''), + color: ko.observable(), + icon: ko.observable(), + groups: [this.showTagsForGroup()], + count: 0 + }); + this.showTagModal(true); + } + + async saveTag(tagToSave) { + + try { + let tag = ko.toJS(tagToSave); + + if (this.exists(tag.name, tag.id)) { + alert(ko.i18nformat('configuration.tagManagement.nameExistsWarning', 'Tag or Group name \'<%=tagName%>\' is already in use.', {tagName: tag.name})()); + return; + } + + if (tag.id !== undefined) { + let oldTag = ko.utils.arrayFirst(this.allTags(), t => t.id === tag.id); + let updatedTag = await TagsService.updateTag(tag); + this.allTags.replace(oldTag, updatedTag.data); + } else { + let newTag = await TagsService.createNewTag(tag); + this.allTags.push(newTag.data); + } + + if (tag.groups.length === 0) { + this.tagGroups(this.allTags().filter(t => !t.groups || t.groups.length === 0)); + + // replace tagGroup in tags + ko.utils.arrayFirst(this.allTags(), t => { + if (t.groups.length > 0 && t.groups[0].id === tag.id) { + t.groups[0] = tag; + } + }); + // rerender tags table + if (this.showTagsForGroup() && this.showTagsForGroup().id === tag.id) { + this.showTagsForGroup(tag); + } + + this.closeGroup(); + } else { + this.tags(this.allTags().filter(t => t.groups && t.groups.length > 0 && t.groups[0].id === tag.groups[0].id)); + this.closeTag(); + } + } catch(e) { + console.log(e); + alert("Error! Check the console.") + } + } + + async deleteTag(tag) { + try { + if (tag.groups.length === 0) { // group + + // check if group is empty + let empty = true; + ko.utils.arrayFirst(this.allTags(), t => { + if (t.groups.length > 0 && t.groups[0].id === tag.id) { + empty = false; + } + }); + + if (!empty) { + alert(ko.i18n('configuration.tagManagement.errorGroupNotEmpty', 'Cannot delete tag group: the group contains tags.')()); + return; + } + + if (!confirm(ko.i18n('configuration.tagManagement.deleteGroupConfirmation', 'Deletion cannot be undone! Are you sure?')())) { + return; + } + } else { // tag + if (!confirm(ko.i18n('configuration.tagManagement.deleteTagConfirmation', 'If the tag is assigned to an asset, it will be unassigned. Deletion cannot be undone. Are you sure you want to delete the tag?')())) { + return; + } + } + + await TagsService.deleteTag(tag); + this.allTags.remove(tag); + + if (tag.groups.length > 0) { // tag + this.tags(this.allTags().filter(t => t.groups && t.groups.length > 0 && t.groups[0].id === tag.groups[0].id)); + } else { // group + this.tagGroups(this.allTags().filter(t => !t.groups || t.groups.length === 0)); + this.showTagsForGroup(null); + } + + } catch(e) { + console.log(e); + alert("Error! Check the console.") + } + } + + exists(tagName, skipId) { + return this.allTags().find(t => t.id !== skipId && t.name.toLowerCase() === tagName.toLowerCase()); + } + + closeGroup() { + this.showTagGroupModal(false); + setTimeout(() => { + this.currentTagGroup({}); + }, 0); + } + + closeTag() { + this.showTagModal(false); + setTimeout(() => { + this.currentTag({}); + }, 0); + } + } + + return commonUtils.build('tag-management', TagManagement, view); +}); diff --git a/js/pages/configuration/tag-management/tag-management.less b/js/pages/configuration/tag-management/tag-management.less new file mode 100644 index 000000000..de9cc2732 --- /dev/null +++ b/js/pages/configuration/tag-management/tag-management.less @@ -0,0 +1,92 @@ +.tag-management { + + &__container { + padding: 10px; + margin-top: 5px; + } + + &__tags-container { + margin-top: 25px; + } + + &__create-button { + margin: 5px 0 10px; + font-size: 12px; + } + + a { + cursor: pointer; + } +} + +td .center-span { + display: block; + text-align: center; + + .fa-check { + color: #333; + border: 0; + background-color: inherit; + cursor: inherit; + } +} + +.tag { + font-size: 13px; + font-weight: 500; + display: inline-block; + position: relative; + background-color: #bdbdbd; + color: black; + border-radius: 2px 2px; + padding: 1px 4px 0 3px; + + .tooltip { + display: none; + border: 1px solid grey; + border-radius: 2px 2px; + padding: 3px 5px; + background-color: white; + font-size: 12px; + top: 20px; + left: 0; + max-width: 200px; + width: max-content; + opacity: 1; + } + + &:hover .tooltip { + display: block; + position: absolute; + } +} + +.col-md-3 { + margin: 0 16px 0 0; + padding: 0; +} + +.checkbox-description { + display: inline-block; + padding-top: 3px; +} + +.tag-example-label { + margin-bottom: 11px; +} + +.modal-footer { + display: table; + width: 100%; + padding: 0; + border: 0; + + .modal-buttons { + display: table-cell; + text-align: right; + + button + button { + margin-left: 10px; + } + } +} \ No newline at end of file diff --git a/js/pages/configuration/users-import/users-import.js b/js/pages/configuration/users-import/users-import.js index 1deee0eae..a1f1a4a24 100644 --- a/js/pages/configuration/users-import/users-import.js +++ b/js/pages/configuration/users-import/users-import.js @@ -184,7 +184,7 @@ define(['knockout', const users = this.usersList() .filter(u => !!u.included()) .map(u => ({ - login: u.login, roles: u.roles(), + login: u.login, displayName: u.displayName, roles: u.roles(), })); userService.importUsers(users, this.importProvider()).then(job => { this.startPolling(job.id); diff --git a/js/pages/data-sources/components/reports/condition.js b/js/pages/data-sources/components/reports/condition.js index 232a063f3..d78b8cf03 100644 --- a/js/pages/data-sources/components/reports/condition.js +++ b/js/pages/data-sources/components/reports/condition.js @@ -1,13 +1,13 @@ define([ 'knockout', 'text!./treemap.html', - 'pages/data-sources/classes/Treemap', + 'components/reports/classes/Treemap', 'components/Component', - 'pages/data-sources/const', + 'components/reports/const', 'utils/CommonUtils', 'components/heading', 'components/charts/treemap', - 'pages/data-sources/components/reports/treemapDrilldown', + 'components/reports/reportDrilldown' ], function ( ko, view, diff --git a/js/pages/data-sources/components/reports/conditionEra.js b/js/pages/data-sources/components/reports/conditionEra.js index 666ae2c07..9dd4c6425 100644 --- a/js/pages/data-sources/components/reports/conditionEra.js +++ b/js/pages/data-sources/components/reports/conditionEra.js @@ -2,12 +2,12 @@ define([ 'knockout', 'text!./treemap.html', 'components/Component', - 'pages/data-sources/classes/Treemap', - 'pages/data-sources/const', + 'components/reports/classes/Treemap', + 'components/reports/const', 'utils/CommonUtils', 'components/heading', 'components/charts/treemap', - 'pages/data-sources/components/reports/treemapDrilldown', + 'components/reports/reportDrilldown' ], function ( ko, view, diff --git a/js/pages/data-sources/components/reports/dashboard.js b/js/pages/data-sources/components/reports/dashboard.js index a7ac92d00..d2e09c270 100644 --- a/js/pages/data-sources/components/reports/dashboard.js +++ b/js/pages/data-sources/components/reports/dashboard.js @@ -6,7 +6,7 @@ define([ 'd3-tip', 'utils/CommonUtils', 'utils/ChartUtils', - 'pages/data-sources/classes/Report', + 'components/reports/classes/Report', 'components/Component', 'components/heading', 'components/charts/donut', @@ -20,8 +20,7 @@ define([ d3tip, commonUtils, ChartUtils, - Report, - Component + Report ) { const FORMAT_VALUES_BIGGER_THAN = 99999; diff --git a/js/pages/data-sources/components/reports/datadensity.js b/js/pages/data-sources/components/reports/datadensity.js index d51252188..ba035d6be 100644 --- a/js/pages/data-sources/components/reports/datadensity.js +++ b/js/pages/data-sources/components/reports/datadensity.js @@ -4,7 +4,7 @@ define([ 'd3', 'utils/CommonUtils', 'utils/ChartUtils', - 'pages/data-sources/classes/Report', + 'components/reports/classes/Report', 'components/Component', 'components/heading', 'components/charts/boxplot', @@ -15,8 +15,7 @@ define([ d3, commonUtils, ChartUtils, - Report, - Component + Report ) { class datadensity extends Report { constructor(params) { diff --git a/js/pages/data-sources/components/reports/death.js b/js/pages/data-sources/components/reports/death.js index c6bb46cb4..5c652dc54 100644 --- a/js/pages/data-sources/components/reports/death.js +++ b/js/pages/data-sources/components/reports/death.js @@ -5,7 +5,7 @@ define([ 'utils/CommonUtils', 'utils/ChartUtils', 'const', - 'pages/data-sources/classes/Report', + 'components/reports/classes/Report', 'components/Component', 'components/heading', 'components/charts/donut', @@ -20,7 +20,6 @@ define([ ChartUtils, constants, Report, - Component ) { class Death extends Report { constructor(params) { diff --git a/js/pages/data-sources/components/reports/drug.js b/js/pages/data-sources/components/reports/drug.js index f765af669..570e7adc4 100644 --- a/js/pages/data-sources/components/reports/drug.js +++ b/js/pages/data-sources/components/reports/drug.js @@ -1,13 +1,13 @@ define([ 'knockout', 'text!./treemap.html', - 'pages/data-sources/classes/Treemap', + 'components/reports/classes/Treemap', 'components/Component', - 'pages/data-sources/const', + 'components/reports/const', 'utils/CommonUtils', 'components/heading', 'components/charts/treemap', - 'pages/data-sources/components/reports/treemapDrilldown', + 'components/reports/reportDrilldown' ], function ( ko, view, diff --git a/js/pages/data-sources/components/reports/drugEra.js b/js/pages/data-sources/components/reports/drugEra.js index 9fc7e9941..b069017e5 100644 --- a/js/pages/data-sources/components/reports/drugEra.js +++ b/js/pages/data-sources/components/reports/drugEra.js @@ -1,13 +1,13 @@ define([ 'knockout', 'text!./treemap.html', - 'pages/data-sources/classes/Treemap', + 'components/reports/classes/Treemap', 'components/Component', - 'pages/data-sources/const', + 'components/reports/const', 'utils/CommonUtils', 'components/heading', 'components/charts/treemap', - 'pages/data-sources/components/reports/treemapDrilldown', + 'components/reports/reportDrilldown' ], function ( ko, view, diff --git a/js/pages/data-sources/components/reports/measurement.js b/js/pages/data-sources/components/reports/measurement.js index 9e50f9af7..3514d3d18 100644 --- a/js/pages/data-sources/components/reports/measurement.js +++ b/js/pages/data-sources/components/reports/measurement.js @@ -1,13 +1,13 @@ define([ 'knockout', 'text!./treemap.html', - 'pages/data-sources/classes/Treemap', + 'components/reports/classes/Treemap', 'components/Component', - 'pages/data-sources/const', + 'components/reports/const', 'utils/CommonUtils', 'components/heading', 'components/charts/treemap', - 'pages/data-sources/components/reports/treemapDrilldown', + 'components/reports/reportDrilldown' ], function ( ko, view, diff --git a/js/pages/data-sources/components/reports/observation-period.js b/js/pages/data-sources/components/reports/observation-period.js index 16e827126..a0d88599d 100644 --- a/js/pages/data-sources/components/reports/observation-period.js +++ b/js/pages/data-sources/components/reports/observation-period.js @@ -6,7 +6,7 @@ define([ "utils/CommonUtils", "utils/ChartUtils", "const", - "pages/data-sources/classes/Report", + 'components/reports/classes/Report', "components/Component", "components/charts/histogram", "components/charts/boxplot", diff --git a/js/pages/data-sources/components/reports/observation.js b/js/pages/data-sources/components/reports/observation.js index 875d22f6f..2f35f2b95 100644 --- a/js/pages/data-sources/components/reports/observation.js +++ b/js/pages/data-sources/components/reports/observation.js @@ -1,13 +1,13 @@ define([ 'knockout', 'text!./treemap.html', - 'pages/data-sources/classes/Treemap', + 'components/reports/classes/Treemap', 'components/Component', - 'pages/data-sources/const', + 'components/reports/const', 'utils/CommonUtils', 'components/heading', 'components/charts/treemap', - 'pages/data-sources/components/reports/treemapDrilldown', + 'components/reports/reportDrilldown' ], function ( ko, view, diff --git a/js/pages/data-sources/components/reports/person.js b/js/pages/data-sources/components/reports/person.js index 241f073a5..1676fc6c5 100644 --- a/js/pages/data-sources/components/reports/person.js +++ b/js/pages/data-sources/components/reports/person.js @@ -5,7 +5,7 @@ define([ 'atlascharts', 'utils/CommonUtils', 'utils/ChartUtils', - 'pages/data-sources/classes/Report', + 'components/reports/classes/Report', 'components/Component', 'components/heading', 'components/charts/histogram', diff --git a/js/pages/data-sources/components/reports/procedure.js b/js/pages/data-sources/components/reports/procedure.js index 6c9a19af8..f5c06281a 100644 --- a/js/pages/data-sources/components/reports/procedure.js +++ b/js/pages/data-sources/components/reports/procedure.js @@ -1,13 +1,13 @@ define([ 'knockout', 'text!./treemap.html', - 'pages/data-sources/classes/Treemap', + 'components/reports/classes/Treemap', 'components/Component', - 'pages/data-sources/const', + 'components/reports/const', 'utils/CommonUtils', 'components/heading', 'components/charts/treemap', - 'pages/data-sources/components/reports/treemapDrilldown', + 'components/reports/reportDrilldown' ], function ( ko, view, diff --git a/js/pages/data-sources/components/reports/treemap.html b/js/pages/data-sources/components/reports/treemap.html index 6a9f529eb..3ce37dcf0 100644 --- a/js/pages/data-sources/components/reports/treemap.html +++ b/js/pages/data-sources/components/reports/treemap.html @@ -51,17 +51,17 @@ - - + \ No newline at end of file diff --git a/js/pages/data-sources/components/reports/visit.js b/js/pages/data-sources/components/reports/visit.js index df6a51d93..aa675022f 100644 --- a/js/pages/data-sources/components/reports/visit.js +++ b/js/pages/data-sources/components/reports/visit.js @@ -1,13 +1,13 @@ define([ 'knockout', 'text!./treemap.html', - 'pages/data-sources/classes/Treemap', + 'components/reports/classes/Treemap', 'components/Component', - 'pages/data-sources/const', + 'components/reports/const', 'utils/CommonUtils', 'components/heading', 'components/charts/treemap', - 'pages/data-sources/components/reports/treemapDrilldown', + 'components/reports/reportDrilldown' ], function ( ko, view, diff --git a/js/pages/data-sources/const.js b/js/pages/data-sources/const.js deleted file mode 100644 index ff71050d3..000000000 --- a/js/pages/data-sources/const.js +++ /dev/null @@ -1,28 +0,0 @@ -define( - (require, factory) => { - const ko = require('knockout'); - const config = require('appConfig'); - - const apiPaths = { - report: ({ sourceKey, path, conceptId }) => `${config.api.url}cdmresults/${sourceKey}/${path}${conceptId !== null ? `/${conceptId}` : ''}`, - }; - - // aggregate property descriptors - const recordsPerPersonProperty = { - name: "recordsPerPerson", - description: ko.i18n('dataSources.const.recordsPerPerson', 'Records per person') - }; - const lengthOfEraProperty = { - name: "lengthOfEra", - description: ko.i18n('dataSources.const.lengthOfEra', 'Length of era') - }; - - return { - apiPaths, - aggProperties: { - byPerson: recordsPerPersonProperty, - byLengthOfEra: lengthOfEraProperty, - }, - }; - } -); \ No newline at end of file diff --git a/js/pages/data-sources/data-sources.html b/js/pages/data-sources/data-sources.html index 838e819f7..3ab301b2a 100644 --- a/js/pages/data-sources/data-sources.html +++ b/js/pages/data-sources/data-sources.html @@ -34,7 +34,7 @@
    diff --git a/js/pages/estimation/cca-manager.html b/js/pages/estimation/cca-manager.html index ebed9b528..05b1d6a02 100644 --- a/js/pages/estimation/cca-manager.html +++ b/js/pages/estimation/cca-manager.html @@ -15,10 +15,12 @@ - + + + @@ -75,4 +77,4 @@ loadRoleSuggestionsFn: $component.loadAccessRoleSuggestions "> - \ No newline at end of file + diff --git a/js/pages/estimation/cca-manager.js b/js/pages/estimation/cca-manager.js index d80a2f699..55ddf0d4c 100644 --- a/js/pages/estimation/cca-manager.js +++ b/js/pages/estimation/cca-manager.js @@ -72,6 +72,7 @@ define([ this.defaultCovariateSettings = ko.observable(); this.options = constants.options; this.config = config; + this.enablePermissionManagement = config.enablePermissionManagement; this.loading = ko.observable(true); this.isAuthenticated = ko.pureComputed(() => { return authApi.isAuthenticated(); @@ -592,4 +593,4 @@ define([ } return commonUtils.build('cca-manager', ComparativeCohortAnalysisManager, view); -}); \ No newline at end of file +}); diff --git a/js/pages/incidence-rates/components/iranalysis/components/results.html b/js/pages/incidence-rates/components/iranalysis/components/results.html index 5378fc638..665fb77a4 100644 --- a/js/pages/incidence-rates/components/iranalysis/components/results.html +++ b/js/pages/incidence-rates/components/iranalysis/components/results.html @@ -1,3 +1,4 @@ +
    Show only sources with results +
    -
    + +
    + + @@ -32,8 +48,7 @@ [+|-]
    -
    - +
    [+|-]
    @@ -121,4 +136,23 @@

    title: 'Execution Exit Message', exitMessage: $component.exitMessage, }"> - \ No newline at end of file + + + \ No newline at end of file diff --git a/js/pages/incidence-rates/components/iranalysis/components/results.js b/js/pages/incidence-rates/components/iranalysis/components/results.js index 9d18bd7d8..e77519f9f 100644 --- a/js/pages/incidence-rates/components/iranalysis/components/results.js +++ b/js/pages/incidence-rates/components/iranalysis/components/results.js @@ -112,6 +112,43 @@ define([ }); this.expandSelectedSource(); + + this.showOnlySourcesWithResults = ko.observable(false); + this.sourcesTableOptions = commonUtils.getTableOptions('S'); + this.sourcesColumns = [{ + title: ko.i18n('cohortDefinitions.cohortDefinitionManager.panels.sourceName', 'Source Name'), + render: (s,p,d) => `${d.source.sourceName}` + }, { + /*title: ko.i18n('cohortDefinitions.cohortDefinitionManager.panels.generationStatus', 'Generation Status'), + render: (s,p,d) => { + return d.info() ? `${d.info().status}` : `n/a` + } + }, { */ + title: ko.i18n('ir.results.persons', 'Persons'), + render: (s,p,d) => d.info() ? `${this.getSummaryData(d.info().summaryList).totalPersons}` : `n/a` + }, { + title: ko.i18n('ir.results.cases', 'Cases'), + render: (s,p,d) => d.info() ? `${this.getSummaryData(d.info().summaryList).cases}` : `n/a` + }, { + title: `${ko.i18n('ir.results.proportion', 'Proportion')()}
    ${this.ipCaption()}`, + render: (s,p,d) => d.info() ? `${this.getSummaryData(d.info().summaryList).proportion}` : `n/a` + }, { + title: `${ko.i18n('ir.results.timeAtRisk', 'Time At Risk')()}
    (years)`, + render: (s,p,d) => d.info() ? `${this.getSummaryData(d.info().summaryList).timeAtRisk}` : `n/a` + }, { + title: `${ko.i18n('ir.results.rate', 'Rate')()}
    ${this.irCaption()}`, + render: (s,p,d) => d.info() ? `${this.getSummaryData(d.info().summaryList).rate}` : `n/a` + }, { + title: ko.i18n('ir.results.started', 'Started'), + render: (s,p,d) => d.info() ? `${this.formatDateTime(d.info().executionInfo.startTime)}` : `n/a` + }, { + title: ko.i18n('ir.results.duration', 'Duration'), + render: (s,p,d) => d.info() ? `${this.msToTime(d.info().executionInfo.executionDuration)}` : `n/a` + }, { + sortable: false, + className: 'generation-buttons-column', + render: () => `` + }]; } reportDisabledReason(source) { diff --git a/js/pages/incidence-rates/components/iranalysis/components/results.less b/js/pages/incidence-rates/components/iranalysis/components/results.less index 0581d2714..fb054e202 100644 --- a/js/pages/incidence-rates/components/iranalysis/components/results.less +++ b/js/pages/incidence-rates/components/iranalysis/components/results.less @@ -1,5 +1,12 @@ .ir-analysis-results { + &__generation-heading { + font-size: 1.8rem; + color: #333; + font-weight: 500; + margin: 12px 0 3px 8px; + } + &__header { align-items: center; display: flex; @@ -7,11 +14,31 @@ margin: 0.5rem 0.5rem 2rem 0.75rem; } + &__only-results-checkbox { + margin-left: 8px; + } + + &__sources-table { + thead tr th, tbody tr:first-child td { + padding: 4px 10px; + } + tbody tr td { + padding: 0 10px 4px; + } + .generation-buttons-column { + text-align: right; + } + .generation-buttons { + white-space: nowrap; + } + } + &__generate-btn { margin-right: 1.5rem; } &__tbl-container { + margin-left: 8px; &--short { max-height: 21rem; overflow: auto; diff --git a/js/pages/incidence-rates/ir-manager.html b/js/pages/incidence-rates/ir-manager.html index 1b2fd0770..ab7f76a96 100644 --- a/js/pages/incidence-rates/ir-manager.html +++ b/js/pages/incidence-rates/ir-manager.html @@ -22,9 +22,11 @@ + + diff --git a/js/pages/incidence-rates/ir-manager.js b/js/pages/incidence-rates/ir-manager.js index c05ac08b6..77db6bb0d 100644 --- a/js/pages/incidence-rates/ir-manager.js +++ b/js/pages/incidence-rates/ir-manager.js @@ -85,6 +85,7 @@ define([ this.selectedAnalysisId = sharedState.IRAnalysis.selectedId; this.previewVersion = sharedState.IRAnalysis.previewVersion; this.dirtyFlag = sharedState.IRAnalysis.dirtyFlag; + this.enablePermissionManagement = config.enablePermissionManagement; this.exporting = ko.observable(); this.isAuthenticated = ko.pureComputed(() => { return authAPI.isAuthenticated(); @@ -465,7 +466,7 @@ define([ if (!this.pollId) { this.pollId = JobPollService.add({ callback: silently => this.pollForInfo({ silently }), - interval: 10000, + interval: config.pollInterval, isSilentAfterFirstCall: true, }); } @@ -581,7 +582,11 @@ define([ removeResult(analysisResult) { if (confirm(ko.i18nformat('ir.deleteResultConfirmation', 'Do you really want to remove result of <%=name%>?', {name:analysisResult.source.sourceName})())) { - IRAnalysisService.deleteInfo(this.selectedAnalysisId(), analysisResult.source.sourceKey).then(() => { + IRAnalysisService.deleteInfo(this.selectedAnalysisId(), analysisResult.source.sourceKey).then((response) => { + if (response.status === 403) { + alert(ko.i18n('ir.deleteResult403Error', 'Only Moderator role can delete generation results. Please check your role.')()); + return; + } const source = this.sources().find(s => s.source.sourceId === analysisResult.source.sourceId); source.info(null); }); diff --git a/js/pages/pathways/PathwayService.js b/js/pages/pathways/PathwayService.js index 9f98ec019..0a56c89d4 100644 --- a/js/pages/pathways/PathwayService.js +++ b/js/pages/pathways/PathwayService.js @@ -1,11 +1,13 @@ define([ 'services/http', 'appConfig', - 'utils/ExecutionUtils' + 'utils/ExecutionUtils', + 'services/AuthAPI', ], function ( httpService, config, - executionUtils, + executionUtils, + authApi ) { const servicePath = config.webAPIRoot + 'pathway-analysis'; @@ -15,22 +17,22 @@ define([ .then(res => res.data); } - function create(design) { - return request = httpService.doPost(servicePath, design).then(res => res.data); + async function create(design) { + return authApi.executeWithRefresh(httpService.doPost(servicePath, design).then(res => res.data)); } - function load(id) { - return httpService + async function load(id) { + return authApi.executeWithRefresh(httpService .doGet(`${servicePath}/${id}`) - .then(res => res.data); + .then(res => res.data)); } function save(id, design) { return httpService.doPut(`${servicePath}/${id}`, design).then(res => res.data); } - function copy(id) { - return httpService.doPost(`${servicePath}/${id}`).then(res => res.data); + async function copy(id) { + return authApi.executeWithRefresh(httpService.doPost(`${servicePath}/${id}`).then(res => res.data)); } function del(id) { @@ -57,10 +59,10 @@ define([ .then(res => res.data); } - function generate(id, sourcekey) { - return httpService + async function generate(id, sourcekey) { + return authApi.executeWithRefresh(httpService .doPost(`${servicePath}/${id}/generation/${sourcekey}`) - .then(res => res.data); + .then(res => res.data)); } function cancelGeneration(id, sourceKey) { @@ -81,10 +83,10 @@ define([ .then(res => res.data); } - function importPathwayDesign(design) { - return httpService + async function importPathwayDesign(design) { + return authApi.executeWithRefresh(httpService .doPost(`${servicePath}/import`, design) - .then(res => res.data); + .then(res => res.data)); } function exists(name, id) { @@ -109,9 +111,9 @@ define([ .then(res => res.data); } - function copyVersion(id, versionNumber) { - return httpService.doPut(`${servicePath}/${id}/version/${versionNumber}/createAsset`) - .then(res => res.data); + async function copyVersion(id, versionNumber) { + return authApi.executeWithRefresh(httpService.doPut(`${servicePath}/${id}/version/${versionNumber}/createAsset`) + .then(res => res.data)); } function updateVersion(version) { diff --git a/js/pages/pathways/components/manager.html b/js/pages/pathways/components/manager.html index 8abbcf19a..70f3f81bf 100644 --- a/js/pages/pathways/components/manager.html +++ b/js/pages/pathways/components/manager.html @@ -21,7 +21,9 @@ + + diff --git a/js/pages/pathways/components/manager.js b/js/pages/pathways/components/manager.js index aaea7ea41..dfb6273d0 100644 --- a/js/pages/pathways/components/manager.js +++ b/js/pages/pathways/components/manager.js @@ -58,6 +58,7 @@ define([ this.design = sharedState.CohortPathways.current; this.previewVersion = sharedState.CohortPathways.previewVersion; this.dirtyFlag = sharedState.CohortPathways.dirtyFlag; + this.enablePermissionManagement = config.enablePermissionManagement; this.executionId = ko.observable(params.router.routerParams().executionId); this.selectedSourceId = ko.observable(params.router.routerParams().sourceId); this.analysisId = ko.observable(); diff --git a/js/pages/pathways/components/tabs/pathway-tableview.html b/js/pages/pathways/components/tabs/pathway-tableview.html index 83d18add7..f96667c75 100644 --- a/js/pages/pathways/components/tabs/pathway-tableview.html +++ b/js/pages/pathways/components/tabs/pathway-tableview.html @@ -37,9 +37,7 @@
    The number of people that have unique event cohorts or conbimations found across all ranks of the pathway analysis results.
    -
    - +

    - \ No newline at end of file diff --git a/js/pages/pathways/components/tabs/pathway-tableview.js b/js/pages/pathways/components/tabs/pathway-tableview.js index 10076d3d9..f86c7809e 100644 --- a/js/pages/pathways/components/tabs/pathway-tableview.js +++ b/js/pages/pathways/components/tabs/pathway-tableview.js @@ -54,23 +54,27 @@ define([ prepareReportData() { const design = this.results.design; const pathwayGroups = this.results.data.pathwayGroups; - return({ cohorts: design.targetCohorts.filter(c => this.filterList.selectedValues().includes(c.id)).map(c => { + const pathwayGroup = pathwayGroups.find(p => p.targetCohortId == c.id); - return { - id: c.id, - name: c.name, - cohortCount: pathwayGroup.targetCohortCount, - pathwayCount: pathwayGroup.totalPathwaysCount, - pathways: pathwayGroup.pathways.map(p => ({ // split pathway paths into paths and counts - path : p.path.split('-') - .filter(step => step != "end") // remove end markers from pathway - .map(p => +p) - .concat(Array(MAX_PATH_LENGTH).fill(null)) // pad end of paths to be at least MAX_PATH_LENGTH - .slice(0,MAX_PATH_LENGTH), // limit path to MAX_PATH_LENGTH. - personCount: p.personCount - })) + if (pathwayGroup) { + return { + id: c.id, + name: c.name, + cohortCount: pathwayGroup.targetCohortCount, + pathwayCount: pathwayGroup.totalPathwaysCount, + pathways: pathwayGroup.pathways.map(p => ({ // split pathway paths into paths and counts + path : p.path.split('-') + .filter(step => step != "end") // remove end markers from pathway + .map(p => +p) + .concat(Array(MAX_PATH_LENGTH).fill(null)) // pad end of paths to be at least MAX_PATH_LENGTH + .slice(0,MAX_PATH_LENGTH), // limit path to MAX_PATH_LENGTH. + personCount: p.personCount + })) + } + } else { + return null; } }), eventCodes: this.results.data.eventCodes @@ -114,7 +118,7 @@ define([ return col; }); let statCols = [columnValueBuilder(ko.i18n('columns.count', 'Count')(), "personCount")]; - let data = this.getPathwayGroupData(pathwayGroup, pathLength); + let data = pathwayGroup ? this.getPathwayGroupData(pathwayGroup, pathLength) : []; statCols.push(columnValueBuilder(ko.i18n('columns.pctWithPathway', '% with Pathway')(), "pathwayPercent", percentFormat)); statCols.push(columnValueBuilder(ko.i18n('columns.pctOfCohort', '% of Cohort')(), "cohortPercent", percentFormat)); @@ -154,6 +158,8 @@ define([ getEventCohortsByRank(pathwayGroup) { + if (!pathwayGroup) return []; // default to empty data + const pathways = pathwayGroup.pathways; const eventCodes = this.reportData().eventCodes; let groups = pathways.reduce((acc,cur) => { // reduce pathways an Array of ranks containing a Map of counts by event cohort @@ -225,6 +231,7 @@ define([ getEventCohortCounts(pathwayGroup) { + if (!pathwayGroup) return []; // default to empty data const pathways = pathwayGroup.pathways; const eventCodes = this.reportData().eventCodes; let dataMap = pathways.reduce((acc,cur) => { // reduce pathways an Array of ranks containing a Map of counts by event cohort @@ -292,6 +299,8 @@ define([ getDistinctEventCohortCounts(pathwayGroup) { + if (!pathwayGroup) return []; // default to empty data + const pathways = pathwayGroup.pathways; let dataMap = pathways.reduce((acc,cur) => { // reduce pathways an Array of ranks containing a Map of counts by comboId const visited = new Map(); diff --git a/js/pages/prediction/prediction-manager.html b/js/pages/prediction/prediction-manager.html index df62a2342..bbbe2754d 100644 --- a/js/pages/prediction/prediction-manager.html +++ b/js/pages/prediction/prediction-manager.html @@ -26,10 +26,13 @@ + + + @@ -88,4 +91,4 @@ loadRoleSuggestionsFn: $component.loadAccessRoleSuggestions "> - \ No newline at end of file + diff --git a/js/pages/prediction/prediction-manager.js b/js/pages/prediction/prediction-manager.js index 57de6e748..ab23b61cf 100644 --- a/js/pages/prediction/prediction-manager.js +++ b/js/pages/prediction/prediction-manager.js @@ -5,7 +5,7 @@ define([ 'pages/Router', 'utils/CommonUtils', 'assets/ohdsi.util', - 'appConfig', + 'appConfig', './const', 'const', 'atlas-state', @@ -81,6 +81,7 @@ define([ this.options = constants.options; this.config = config; + this.enablePermissionManagement = config.enablePermissionManagement; this.loading = ko.observable(true); this.patientLevelPredictionAnalysis = sharedState.predictionAnalysis.current; this.selectedAnalysisId = sharedState.predictionAnalysis.selectedId; @@ -491,4 +492,4 @@ define([ } return commonUtils.build('prediction-manager', PatientLevelPredictionManager, view); -}); \ No newline at end of file +}); diff --git a/js/pages/reusables/components/manager.html b/js/pages/reusables/components/manager.html index 7964dc20b..55eb76054 100644 --- a/js/pages/reusables/components/manager.html +++ b/js/pages/reusables/components/manager.html @@ -21,7 +21,9 @@ + + diff --git a/js/pages/reusables/components/manager.js b/js/pages/reusables/components/manager.js index faa02c0fb..17b2ff184 100644 --- a/js/pages/reusables/components/manager.js +++ b/js/pages/reusables/components/manager.js @@ -86,9 +86,9 @@ define([ this.canDelete = this.isDeletePermittedResolver(); this.isNewEntity = this.isNewEntityResolver(); this.canCopy = this.canCopyResolver(); - this.selectedTabKey = ko.observable("design"); - + this.enablePermissionManagement = config.enablePermissionManagement; + this.componentParams = ko.observable({ design: this.previewVersion() ? this.previewVersion : this.design, designId: this.designId, diff --git a/js/pages/vocabulary/components/search.html b/js/pages/vocabulary/components/search.html index a34fa2d92..1eb8ba5d1 100644 --- a/js/pages/vocabulary/components/search.html +++ b/js/pages/vocabulary/components/search.html @@ -79,7 +79,7 @@ options: resultSources, optionsText: 'sourceName', optionsValue: 'sourceKey', - value: currentResultSource().sourceKey, + value: currentResultSourceKey, event: { change: refreshRecordCounts } "> @@ -117,7 +117,7 @@ diff --git a/js/pages/vocabulary/components/search.js b/js/pages/vocabulary/components/search.js index 41650526a..1f17c040d 100644 --- a/js/pages/vocabulary/components/search.js +++ b/js/pages/vocabulary/components/search.js @@ -14,7 +14,6 @@ define([ 'utils/CommonUtils', 'services/Vocabulary', 'components/conceptset/ConceptSetStore', - 'const', 'components/tabs', 'components/panel', 'faceted-datatable', @@ -38,7 +37,6 @@ define([ commonUtils, vocabularyProvider, ConceptSetStore, - globalConstants, ) { class Search extends AutoBind(Component) { constructor(params) { @@ -265,14 +263,14 @@ define([ }); } - this.currentResultSource = ko.observable(); + this.currentResultSourceKey = ko.observable(); this.resultSources = ko.computed(() => { const resultSources = []; sharedState.sources().forEach((source) => { if (source.hasResults && authApi.isPermittedAccessSource(source.sourceKey)) { resultSources.push(source); if (source.resultsUrl === sharedState.resultsUrl()) { - this.currentResultSource(source); + this.currentResultSourceKey(source.sourceKey); } } }) @@ -434,7 +432,7 @@ define([ this.searchExecuted(true); // signals 'no results found' message return; } - await vocabularyProvider.loadDensity(recommendedConcepts, this.currentResultSource().sourceKey,(v)=>parseInt(v,10)); // formatting values as ints + await vocabularyProvider.loadDensity(recommendedConcepts, this.currentResultSourceKey(),(v)=>parseInt(v,10)); // formatting values as ints recommendedConcepts.sort((a,b) => b.DESCENDANT_RECORD_COUNT - a.DESCENDANT_RECORD_COUNT); // sort descending order by DRC const conceptSetStore = ConceptSetStore.repository(); const items = commonUtils.buildConceptSetItems([recommendedConcepts[0]], {includeDescendants: true}); @@ -470,7 +468,7 @@ define([ throw { message: 'No results found', results }; } - const promise = vocabularyProvider.loadDensity(results, this.currentResultSource().sourceKey); + const promise = vocabularyProvider.loadDensity(results, this.currentResultSourceKey()); promise.then(() => { this.data(this.normalizeSearchResults(results)); }); @@ -478,12 +476,8 @@ define([ return promise; } - addConcepts(options, conceptSetStore = ConceptSetStore.repository()) { - sharedState.activeConceptSet(conceptSetStore); - const concepts = commonUtils.getSelectedConcepts(this.data); - const items = commonUtils.buildConceptSetItems(concepts, options); - conceptSetUtils.addItemsToConceptSet({items, conceptSetStore}); - commonUtils.clearConceptsSelectionState(this.data); + getSelectedConcepts() { + return commonUtils.getSelectedConcepts(this.data) } getVocabularies() { @@ -520,10 +514,12 @@ define([ return; } + this.currentResultSourceKey(event.target.value); + this.recordCountsRefreshing(true); this.columnHeadersWithIcons.forEach(c => this.toggleCountColumnHeaderSpin(c, true)); const results = this.data(); - await vocabularyProvider.loadDensity(results, this.currentResultSource().sourceKey); + await vocabularyProvider.loadDensity(results, this.currentResultSourceKey()); this.data(results); this.columnHeadersWithIcons.forEach(c => this.toggleCountColumnHeaderSpin(c, false)); this.recordCountsRefreshing(false); diff --git a/js/pages/vocabulary/components/search.less b/js/pages/vocabulary/components/search.less index ca0900aee..53e9c0efc 100644 --- a/js/pages/vocabulary/components/search.less +++ b/js/pages/vocabulary/components/search.less @@ -2,4 +2,30 @@ &__clear-btn { margin-bottom: 10px; } +} + +.preview-modal { + .modal-body { + padding: 0; + } + + .modal-footer { + display: table; + width: 100%; + padding: 0; + border: 0; + + .modal-buttons { + display: table-cell; + text-align: right; + + button + button { + margin-left: 10px; + } + } + + &.with-padding { + padding: 0 11px 10px 0; + } + } } \ No newline at end of file diff --git a/js/services/AuthAPI.js b/js/services/AuthAPI.js index c2779a6fa..b81a7c448 100644 --- a/js/services/AuthAPI.js +++ b/js/services/AuthAPI.js @@ -88,6 +88,9 @@ define(function(require, exports) { if (err.status === 401) { console.log('User is not authed'); subject(null); + if (config.enableSkipLogin) { + signInOpened(true); + } resolve(); } else { reject('Cannot retrieve user info'); @@ -479,6 +482,10 @@ define(function(require, exports) { return isPermitted(`cache:clear:get`); }; + const isPermittedTagsManagement = function () { + return isPermitted(`tag:management`); + }; + const isPermittedRunAs = () => isPermitted('user:runas:post'); const isPermittedViewDataSourceReport = sourceKey => isPermitted(`cdmresults:${sourceKey}:*:get`); @@ -508,6 +515,12 @@ define(function(require, exports) { }); }; + const executeWithRefresh = async function(httpPromise) { + const result = await httpPromise; + await refreshToken(); + return result; + } + var api = { AUTH_PROVIDERS: AUTH_PROVIDERS, AUTH_CLIENTS: AUTH_CLIENTS, @@ -596,6 +609,7 @@ define(function(require, exports) { isPermittedImportUsers, hasSourceAccess, isPermittedRunAs, + isPermittedTagsManagement, isPermittedClearServerCache, isPermittedViewDataSourceReport, isPermittedViewDataSourceReportDetails, @@ -603,6 +617,7 @@ define(function(require, exports) { loadUserInfo, TOKEN_HEADER, runAs, + executeWithRefresh, }; return api; diff --git a/js/services/CohortDefinition.js b/js/services/CohortDefinition.js index 0d464d09b..13ec4a567 100644 --- a/js/services/CohortDefinition.js +++ b/js/services/CohortDefinition.js @@ -22,8 +22,8 @@ define(function (require, exports) { return promise; } - function saveCohortDefinition(definition) { - var savePromise = $.ajax({ + async function saveCohortDefinition(definition) { + return authApi.executeWithRefresh($.ajax({ url: config.webAPIRoot + 'cohortdefinition/' + (definition.id || ""), method: definition.id ? 'PUT' : 'POST', contentType: 'application/json', @@ -32,12 +32,11 @@ define(function (require, exports) { console.log("Error: " + error); authApi.handleAccessDenied(error); } - }); - return savePromise; + })); } - function copyCohortDefinition(id) { - var copyPromise = $.ajax({ + async function copyCohortDefinition(id) { + return authApi.executeWithRefresh($.ajax({ url: config.webAPIRoot + 'cohortdefinition/' + (id || "") +"/copy", method: 'GET', contentType: 'application/json', @@ -45,8 +44,7 @@ define(function (require, exports) { console.log("Error: " + error); authApi.handleAccessDenied(error); } - }); - return copyPromise; + })); } function deleteCohortDefinition(id) { @@ -57,8 +55,8 @@ define(function (require, exports) { return deletePromise; } - function getCohortDefinition(id) { - return httpService + async function getCohortDefinition(id) { + return authApi.executeWithRefresh(httpService .doGet(config.webAPIRoot + 'cohortdefinition/' + id) .then(res => { const cohortDef = res.data; @@ -67,7 +65,7 @@ define(function (require, exports) { }).catch(error => { console.log("Error: " + error); authApi.handleAccessDenied(error); - }); + })); } function exists(name, id) { @@ -173,8 +171,9 @@ define(function (require, exports) { } function copyVersion(cohortDefinitionId, versionNumber) { - return httpService.doPut(`${config.webAPIRoot}cohortdefinition/${cohortDefinitionId}/version/${versionNumber}/createAsset`) - .then(({ data }) => data); + return authApi.executeWithRefresh(httpService + .doPut(`${config.webAPIRoot}cohortdefinition/${cohortDefinitionId}/version/${versionNumber}/createAsset`) + .then(({ data }) => data)); } function updateVersion(version) { diff --git a/js/services/ConceptSet.js b/js/services/ConceptSet.js index cbac0d215..43e187fc1 100644 --- a/js/services/ConceptSet.js +++ b/js/services/ConceptSet.js @@ -10,8 +10,8 @@ define(function (require) { const _ = require('lodash'); const hash = require('hash-it').default; - function loadConceptSet(id) { - return httpService.doGet(config.api.url + 'conceptset/' + id).then(({ data }) => data); + async function loadConceptSet(id) { + return authApi.executeWithRefresh(httpService.doGet(config.api.url + 'conceptset/' + id).then(({ data }) => data)); } function loadConceptSetExpression(conceptSetId) { @@ -75,8 +75,8 @@ define(function (require) { .then(({ data }) => data); } - function saveConceptSet(conceptSet) { - let promise = new Promise(r => r()); + async function saveConceptSet(conceptSet) { + let promise; const url = `${config.api.url}conceptset/${conceptSet.id ? conceptSet.id : ''}`; if (conceptSet.id) { promise = httpService.doPut(url, conceptSet); @@ -129,9 +129,10 @@ define(function (require) { return httpService.doGet(`${config.webAPIRoot}conceptset/${conceptSetId}/version/${versionNumber}/expression` + (sourceKey ? `/${sourceKey}`: '')).then(({ data }) => data); } - function copyVersion(conceptSetId, versionId) { - return httpService.doPut(`${config.webAPIRoot}conceptset/${conceptSetId}/version/${versionId}/createAsset`) - .then(({ data }) => data); + async function copyVersion(conceptSetId, versionId) { + return authApi.executeWithRefresh(httpService + .doPut(`${config.webAPIRoot}conceptset/${conceptSetId}/version/${versionId}/createAsset`) + .then(({ data }) => data)); } function updateVersion(version) { diff --git a/js/services/Estimation.js b/js/services/Estimation.js index d64eea571..af9f18cb3 100644 --- a/js/services/Estimation.js +++ b/js/services/Estimation.js @@ -2,35 +2,40 @@ define(function (require, exports) { const config = require('appConfig'); const authApi = require('services/AuthAPI'); - const httpService = require('services/http'); - const estimationEndpoint = "estimation/" + const httpService = require('services/http'); + const estimationEndpoint = "estimation/" function getEstimationList() { return httpService.doGet(config.webAPIRoot + estimationEndpoint).catch(authApi.handleAccessDenied); } - function saveEstimation(analysis) { + async function saveEstimation(analysis) { const url = config.webAPIRoot + estimationEndpoint + (analysis.id || ""); - let promise; + let result; if (analysis.id) { - promise = httpService.doPut(url, analysis); + result = await httpService + .doPut(url, analysis) + .catch((error) => { + console.log("Error: " + error); + authApi.handleAccessDenied(error); + }) } else { - promise = httpService.doPost(url, analysis); + result = authApi.executeWithRefresh(httpService + .doPost(url, analysis) + .catch((error) => { + console.log("Error: " + error); + authApi.handleAccessDenied(error); + })) } - promise.catch((error) => { - console.log("Error: " + error); - authApi.handleAccessDenied(error); - }); - - return promise; + return result; } - function copyEstimation(id) { - return httpService.doGet(config.webAPIRoot + estimationEndpoint + (id || "") + "/copy") + async function copyEstimation(id) { + return authApi.executeWithRefresh(httpService.doGet(config.webAPIRoot + estimationEndpoint + (id || "") + "/copy") .catch((error) => { console.log("Error: " + error); authApi.handleAccessDenied(error); - }); + })); } function deleteEstimation(id) { @@ -41,12 +46,13 @@ define(function (require, exports) { }); } - function getEstimation(id) { - return httpService.doGet(config.webAPIRoot + estimationEndpoint + id) + async function getEstimation(id) { + return authApi.executeWithRefresh(httpService + .doGet(config.webAPIRoot + estimationEndpoint + id) .catch((error) => { console.log("Error: " + error); authApi.handleAccessDenied(error); - }); + })); } function exportEstimation(id) { @@ -72,11 +78,11 @@ define(function (require, exports) { .catch(error => authApi.handleAccessDenied(error)); } - function importEstimation(specification) { - return httpService - .doPost(config.webAPIRoot + estimationEndpoint + "import", specification) - .then(res => res.data); - } + function importEstimation(specification) { + return authApi.executeWithRefresh(httpService + .doPost(config.webAPIRoot + estimationEndpoint + "import", specification) + .then(res => res.data)); + } function exists(name, id) { return httpService diff --git a/js/services/IRAnalysis.js b/js/services/IRAnalysis.js index 680f32c88..19daf1088 100644 --- a/js/services/IRAnalysis.js +++ b/js/services/IRAnalysis.js @@ -32,48 +32,54 @@ define(function (require, exports) { return promise; } - function getAnalysis(id) { - const promise = httpService.doGet(`${config.webAPIRoot}ir/${id}`) + async function getAnalysis(id) { + return authApi.executeWithRefresh(httpService.doGet(`${config.webAPIRoot}ir/${id}`) .then(parse) .catch(response => { authApi.handleAccessDenied(response); return response; - }); - - return promise; + })); } - function saveAnalysis(definition) { + async function saveAnalysis(definition) { var definitionCopy = JSON.parse(ko.toJSON(definition)); if (typeof definitionCopy.expression != 'string') definitionCopy.expression = JSON.stringify(definitionCopy.expression); const url = `${config.webAPIRoot}ir/${definitionCopy.id || ""}`; - let promise = new Promise(r => r()); + let result; if (definitionCopy.id) { - promise = httpService.doPut(url, definitionCopy); + result = await httpService + .doPut(url, definitionCopy) + .catch(response => { + authApi.handleAccessDenied(response); + return response; + }) + .then(parse); } else { - promise = httpService.doPost(url, definitionCopy); + result = authApi.executeWithRefresh(httpService + .doPost(url, definitionCopy) + .catch(response => { + authApi.handleAccessDenied(response); + return response; + }) + .then(parse)); } - return promise - .then(parse) - .catch(response => { - authApi.handleAccessDenied(response); - return response; - }); + return result; + } - function copyAnalysis(id) { + async function copyAnalysis(id) { const promise = httpService.doGet(`${config.webAPIRoot}ir/${id || ""}/copy`); - return promise + return authApi.executeWithRefresh(promise .then(parse) .catch(response => { authApi.handleAccessDenied(response); return response; - }); + })); } function deleteAnalysis(id) { diff --git a/js/services/PatientLevelPrediction.js b/js/services/PatientLevelPrediction.js deleted file mode 100644 index c4cb3271f..000000000 --- a/js/services/PatientLevelPrediction.js +++ /dev/null @@ -1,60 +0,0 @@ -define(function (require, exports) { - - const config = require('appConfig'); - const authApi = require('services/AuthAPI'); - const httpService = require('services/http'); - - function getPlpList() { - return httpService.doGet(config.webAPIRoot + 'plp/').catch(authApi.handleAccessDenied); - } - - function savePlp(analysis) { - const url = config.webAPIRoot + 'plp/' + (analysis.analysisId || ""); - let promise; - if (analysis.analysisId) { - promise = httpService.doPut(url, analysis); - } else { - promise = httpService.doPost(url, analysis); - } - promise.catch((error) => { - console.log("Error: " + error); - authApi.handleAccessDenied(error); - }); - - return promise; - } - - function copyPlp(id) { - return httpService.doGet(config.webAPIRoot + 'plp/' + (id || "") + "/copy") - .catch((error) => { - console.log("Error: " + error); - authApi.handleAccessDenied(error); - }); - } - - function deletePlp(id) { - return httpService.doDelete(config.webAPIRoot + 'plp/' + (id || "")) - .catch((error) => { - console.log("Error: " + error); - authApi.handleAccessDenied(error); - }); - } - - function getPlp(id) { - return httpService.doGet(config.webAPIRoot + 'plp/' + id) - .catch((error) => { - console.log("Error: " + error); - authApi.handleAccessDenied(error); - }); - } - - var api = { - getPlpList: getPlpList, - savePlp: savePlp, - copyPlp: copyPlp, - deletePlp: deletePlp, - getPlp: getPlp, - }; - - return api; -}); diff --git a/js/services/Permission.js b/js/services/Permission.js index 52a907887..3b44ea453 100644 --- a/js/services/Permission.js +++ b/js/services/Permission.js @@ -10,27 +10,27 @@ define(function (require, exports) { return res.data; } - async function loadEntityAccessList(entityType, entityId) { - const res = await httpService.doGet(config.webAPIRoot + `permission/access/${entityType}/${entityId}`); + async function loadEntityAccessList(entityType, entityId, perm_type = 'WRITE') { + const res = await httpService.doGet(config.webAPIRoot + `permission/access/${entityType}/${entityId}/${perm_type}`); return res.data; } - function grantEntityAccess(entityType, entityId, roleId) { + function grantEntityAccess(entityType, entityId, roleId, perm_type = 'WRITE') { return httpService.doPost( config.webAPIRoot + `permission/access/${entityType}/${entityId}/role/${roleId}`, { - accessType: 'WRITE' + accessType: perm_type } ); } - function revokeEntityAccess(entityType, entityId, roleId) { - return httpService.doDelete( + function revokeEntityAccess(entityType, entityId, roleId, perm_type = 'WRITE') { + return httpService.doDelete( config.webAPIRoot + `permission/access/${entityType}/${entityId}/role/${roleId}`, { - accessType: 'WRITE' + accessType: perm_type } - ); + ); } function decorateComponent(component, { entityTypeGetter, entityIdGetter, createdByUsernameGetter }) { @@ -43,16 +43,16 @@ define(function (require, exports) { component.isOwner = ko.computed(() => config.userAuthenticationEnabled && component.isOwnerFn(authApi.subject())); - component.loadAccessList = () => { - return loadEntityAccessList(entityTypeGetter(), entityIdGetter()); + component.loadAccessList = (perm_type='WRITE') => { + return loadEntityAccessList(entityTypeGetter(), entityIdGetter(), perm_type); }; - component.grantAccess = (roleId) => { - return grantEntityAccess(entityTypeGetter(), entityIdGetter(), roleId); + component.grantAccess = (roleId, perm_type='WRITE') => { + return grantEntityAccess(entityTypeGetter(), entityIdGetter(), roleId, perm_type); }; - component.revokeAccess = (roleId) => { - return revokeEntityAccess(entityTypeGetter(), entityIdGetter(), roleId); + component.revokeAccess = (roleId, perm_type='WRITE') => { + return revokeEntityAccess(entityTypeGetter(), entityIdGetter(), roleId, perm_type); }; component.loadAccessRoleSuggestions = (searchStr) => { diff --git a/js/services/Prediction.js b/js/services/Prediction.js index e7664d33d..348098937 100644 --- a/js/services/Prediction.js +++ b/js/services/Prediction.js @@ -9,28 +9,30 @@ define(function (require, exports) { return httpService.doGet(config.webAPIRoot + predictionEndpoint).catch(authApi.handleAccessDenied); } - function savePrediction(analysis) { + async function savePrediction(analysis) { const url = config.webAPIRoot + predictionEndpoint + (analysis.id || ""); - let promise; + let result; if (analysis.id) { - promise = httpService.doPut(url, analysis); + result = await httpService.doPut(url, analysis).catch((error) => { + console.log("Error: " + error); + authApi.handleAccessDenied(error); + }); } else { - promise = httpService.doPost(url, analysis); + result = authApi.executeWithRefresh(httpService.doPost(url, analysis).catch((error) => { + console.log("Error: " + error); + authApi.handleAccessDenied(error); + })); } - promise.catch((error) => { - console.log("Error: " + error); - authApi.handleAccessDenied(error); - }); - return promise; + return result; } function copyPrediction(id) { - return httpService.doGet(config.webAPIRoot + predictionEndpoint + (id || "") + "/copy") + return authApi.executeWithRefresh(httpService.doGet(config.webAPIRoot + predictionEndpoint + (id || "") + "/copy") .catch((error) => { console.log("Error: " + error); authApi.handleAccessDenied(error); - }); + })); } function deletePrediction(id) { @@ -42,11 +44,11 @@ define(function (require, exports) { } function getPrediction(id) { - return httpService.doGet(config.webAPIRoot + predictionEndpoint + id) + return authApi.executeWithRefresh(httpService.doGet(config.webAPIRoot + predictionEndpoint + id) .catch((error) => { console.log("Error: " + error); authApi.handleAccessDenied(error); - }); + })); } function exportPrediction(id) { @@ -72,11 +74,11 @@ define(function (require, exports) { .catch(error => authApi.handleAccessDenied(error)); } - function importPrediction(specification) { - return httpService - .doPost(config.webAPIRoot + predictionEndpoint + "import", specification) - .then(res => res.data); - } + async function importPrediction(specification) { + return authApi.executeWithRefresh(httpService + .doPost(config.webAPIRoot + predictionEndpoint + "import", specification) + .then(res => res.data)); + } function exists(name, id) { return httpService diff --git a/js/services/ReusablesService.js b/js/services/ReusablesService.js index e57c95faf..ea4277063 100644 --- a/js/services/ReusablesService.js +++ b/js/services/ReusablesService.js @@ -1,11 +1,13 @@ define([ 'knockout', './http', - 'appConfig', + 'appConfig', + 'services/AuthAPI', ], function ( ko, httpService, config, + authApi, ) { const servicePath = config.webAPIRoot + 'reusable'; @@ -34,7 +36,9 @@ define([ initialEventExpression: design.initialEventExpression, censoringEventExpression: design.censoringEventExpression, }); - return request = httpService.doPost(servicePath, design).then(res => res.data); + let promise = httpService.doPost(servicePath, design).then(res => res.data); + promise.then(authApi.refreshToken); + return promise; } function exists(name, id) { @@ -54,7 +58,9 @@ define([ } function copy(id) { - return httpService.doPost(`${servicePath}/${id}`).then(res => res.data); + let promise = httpService.doPost(`${servicePath}/${id}`).then(res => res.data); + promise.then(authApi.refreshToken); + return promise; } function del(id) { diff --git a/js/services/Tags.js b/js/services/Tags.js index aa6086f46..9928ed026 100644 --- a/js/services/Tags.js +++ b/js/services/Tags.js @@ -54,6 +54,18 @@ define(function (require) { return httpService.doPost(config.webAPIRoot + `tag/`, tag); } + function getTag(id) { + return httpService.doGet(config.webAPIRoot + `tag/${id}`); + } + + function updateTag(tag) { + return httpService.doPut(config.webAPIRoot + `tag/${tag.id}`, tag); + } + + function deleteTag(tag) { + return httpService.doDelete(config.webAPIRoot + `tag/${tag.id}`); + } + function checkPermissionForAssignProtectedTag(assetType, assetId) { return authService.isPermitted(`${assetType}:${assetId}:protectedtag:post`); } @@ -139,9 +151,13 @@ define(function (require) { unassignTag, loadTagsSuggestions, decorateComponent, - getAssignmentPermissions, loadAvailableTags, + getAssignmentPermissions, multiAssign, - multiUnassign + multiUnassign, + createNewTag, + getTag, + updateTag, + deleteTag }; }); \ No newline at end of file diff --git a/js/services/Vocabulary.js b/js/services/Vocabulary.js index 454811416..01e935d6e 100644 --- a/js/services/Vocabulary.js +++ b/js/services/Vocabulary.js @@ -189,6 +189,20 @@ define(function (require, exports) { return getComparedConceptSetPromise; } + + function compareConceptSetCsv(compareTargets,types, url, sourceKey) { + const vocabUrl = getVocabUrl(url, sourceKey); + + var getComparedConceptSetPromise = $.ajax({ + url: vocabUrl + 'compare-arbitrary', + data: JSON.stringify({compareTargets: compareTargets, types:types}), + method: 'POST', + contentType: 'application/json', + error: authAPI.handleAccessDenied, + }); + + return getComparedConceptSetPromise; + } async function loadAncestors(ancestors, descendants, url, sourceKey) { const vocabUrl = getVocabUrl(url, sourceKey); @@ -210,6 +224,7 @@ define(function (require, exports) { getConceptSetExpressionSQL: getConceptSetExpressionSQL, optimizeConceptSet: optimizeConceptSet, compareConceptSet: compareConceptSet, + compareConceptSetCsv: compareConceptSetCsv, loadDensity: loadDensity, loadAncestors, } diff --git a/js/settings.js b/js/settings.js index c18563d28..ec02fb064 100644 --- a/js/settings.js +++ b/js/settings.js @@ -75,9 +75,10 @@ const settingsObject = { ] }, "prism": { - "prism": { - "exports": "Prism" - } + exports: "Prism" + }, + "prismlanguages/prism-sql": { + deps: ["prism"] }, "xss": { exports: "filterXSS" @@ -90,8 +91,9 @@ const settingsObject = { 'd3-color': 'd3', 'd3-interpolate': 'd3', 'd3-selection': 'd3', + 'd3-transition': 'd3', 'd3-collection': 'd3', - 'services/VocabularyProvider': 'services/Vocabulary' + 'services/VocabularyProvider': 'services/Vocabulary', } }, paths: { @@ -119,6 +121,8 @@ const settingsObject = { "localStorageExtender": "assets/localStorageExtender", "appConfig": "config", "prism": "../node_modules/prismjs/prism", + "prismlanguages": "../node_modules/prismjs/components", + "papaparse": "../node_modules/papaparse/papaparse", "js-cookie": "../node_modules/js-cookie/src/js.cookie", "d3": "../node_modules/d3/build/d3", "d3-tip": "../node_modules/d3-tip/dist/index", @@ -143,7 +147,8 @@ const settingsObject = { "ajv": "../node_modules/ajv/dist/ajv.bundle", "hash-it": "../node_modules/hash-it/dist/hash-it.min", "leaflet": "../node_modules/leaflet/dist/leaflet", - "html2canvas": "../node_modules/html2canvas/dist/html2canvas.min" + "html2canvas": "../node_modules/html2canvas/dist/html2canvas.min", + "venn": "../node_modules/venn.js/venn" }, cssPaths: { "font-awesome.min.css": "css!styles/font-awesome.min.css", diff --git a/js/styles/atlas.css b/js/styles/atlas.css index 224231c26..55fd96047 100644 --- a/js/styles/atlas.css +++ b/js/styles/atlas.css @@ -2036,7 +2036,7 @@ button i.fa.fa-question-circle { } -div#wrapper_ohdsi { +div.wrapper_ohdsi { background-color: #eee; width:175px; text-align: center; @@ -2051,11 +2051,11 @@ div#wrapper_ohdsi { flex-grow: 0; } -div#wrapper_ohdsi img { +div.wrapper_ohdsi img { width:100px; } -div#wrapper_ohdsi a { +div.wrapper_ohdsi a { text-decoration:underline; color: #003142; } diff --git a/js/styles/cartoon.css b/js/styles/cartoon.css index dd04b23db..2190a217f 100644 --- a/js/styles/cartoon.css +++ b/js/styles/cartoon.css @@ -224,7 +224,7 @@ div#cartoon-tooltip { position: relative; } div#cartoon-tooltip > div#tooltip { - display: none; + display: none; position: absolute; color: #FFFFFF; background: #000000; diff --git a/js/utils/BrowserDetection.js b/js/utils/BrowserDetection.js deleted file mode 100644 index 176354bac..000000000 --- a/js/utils/BrowserDetection.js +++ /dev/null @@ -1,9 +0,0 @@ - -const browserInfo = bowser.getParser(navigator.userAgent).getBrowser(); -const isBrowserSupported = browserInfo.name.toLowerCase() === 'chrome' && parseInt(browserInfo.version) > 63; -toggleWarning(isBrowserSupported); - -function toggleWarning(doHide) { - - document.getElementById("b-alarm").style.display = (doHide ? "none" : "flex"); -} \ No newline at end of file diff --git a/js/utils/CsvUtils.js b/js/utils/CsvUtils.js index 3eae52590..07429d5f1 100644 --- a/js/utils/CsvUtils.js +++ b/js/utils/CsvUtils.js @@ -1,5 +1,5 @@ define( - ['file-saver'], + ['file-saver','papaparse'], function() { class CsvUtils { /** @@ -81,8 +81,37 @@ define( const blob = new Blob([csvText], {type: "text/csv;charset=utf-8"}); saveAs(blob, fileName || 'data.csv'); } - } + static csvToJson(file, requiredHeader = null) { + const Papa = require('papaparse'); + const regex = /^([\w|\W])+(csv|application\/vnd\.openxmlformats-officedocument\.spreadsheetml\.sheet|application\/vnd\.ms-excel)$/;//regex for the check valid files + if (!regex.test(file.type)) { + return alert("Select a valid CSV File."); + } + const reader = new FileReader(); + + const parsedFile = new Promise((resolve, reject) => { + reader.onload = function (e) { + const file = Papa.parse(e.target.result, { + header: true, + skipEmptyLines: true, + }); + + if (requiredHeader) { + const header = requiredHeader.every(head => file.meta.fields.includes(head)); + if (!header) { + alert('Select a valid CSV File with required headers'); + reject('Select a valid CSV File with required headers'); + } + } + resolve(file.data); + }; + reader.readAsText(file); + }); + return parsedFile; + } + + } return CsvUtils; } diff --git a/js/utils/HighLightUtils.js b/js/utils/HighLightUtils.js new file mode 100644 index 000000000..349bde44a --- /dev/null +++ b/js/utils/HighLightUtils.js @@ -0,0 +1,16 @@ +define(['prismlanguages/prism-sql'], function () { + + function highlightJS(code, language) { + if (!code) { + return ; + } + if (typeof code === 'function') { + return Prism.highlight(code(), Prism.languages[language], language); + } + return Prism.highlight(code, Prism.languages[language], language); + } + + + return highlightJS; + +}); \ No newline at end of file diff --git a/package.json b/package.json index a91e25aff..4eedb1fdd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "atlas", - "version": "2.13.0-DEV", + "version": "2.14.0-DEV", "description": "is an open source software tool for researchers to conduct scientific analyses on standardized observational data converted to the OMOP Common Data Model V5", "main": "js/main.js", "scripts": { @@ -34,63 +34,65 @@ }, "homepage": "https://github.com/OHDSI/Atlas#readme", "devDependencies": { - "@babel/core": "^7.1.2", - "@babel/plugin-proposal-object-rest-spread": "^7.0.0", - "@babel/polyfill": "^7.0.0", - "@babel/preset-env": "^7.1.5", - "esprima": "^4.0.1", - "genversion": "^2.2.0", - "html-document": "^0.8.1", - "requirejs": "^2.3.6", - "rimraf": "^2.6.2", - "terser": "3.17.0" + "@babel/core": "7.21.0", + "@babel/plugin-proposal-object-rest-spread": "7.20.7", + "@babel/polyfill": "7.12.1", + "@babel/preset-env": "7.20.2", + "esprima": "4.0.1", + "genversion": "2.2.0", + "html-document": "0.8.1", + "requirejs": "2.3.6", + "rimraf": "2.6.2", + "terser": "5.16.4" }, "dependencies": { - "@ohdsi/atlascharts": "2.0.1", - "@ohdsi/ui-toolbox": "^1.0.3", - "@ohdsi/visibilityjs": "^2.0.2", - "ajv": "^6.10.0", - "bootstrap": "^3.3.7", - "bootstrap-select": "1.13.6", - "bowser": "2.0.0-beta.3", - "clipboard": "^1.5.16", - "colorbrewer": "^1.3.0", - "crossfilter2": "^1.4.6", - "d3": "^4.10.0", - "d3-scale-chromatic": "^1.3.3", - "d3-tip": "^0.9.1", - "datatables": "^1.10.0", - "datatables.net": "^1.10.19", - "datatables.net-buttons": "^1.5.4", - "datatables.net-select": "^1.3.1", - "director": "^1.2.8", - "eonasdan-bootstrap-datetimepicker": "^4.17.47", - "express": "^4.17.1", - "facets": "^0.1.1", - "file-saver": "^1.3.8", - "hash-it": "~4.0.4", - "html2canvas": "^1.0.0-rc.7", - "jquery": "^1.11.2", - "jquery-ui": "^1.12.1", - "jquery-ui.autocomplete.scroll": "^0.1.9", - "js-cookie": "^2.2.0", - "jszip": "^3.5.0", - "knockout": "3.4.2", - "knockout-mapping": "^2.6.0", - "knockout-sortable": "^1.1.0", - "leaflet": "^1.6.0", - "less": "^3.8.1", - "lodash": "^4.17.11", - "lscache": "^1.3.0", - "lz-string": "^1.4.4", - "moment": "^2.22.2", - "numeral": "^2.0.6", - "ouanalyse-datatables.net-buttons-html5": "^1.5.3", - "prismjs": "^1.27.0", - "qs": "^6.5.2", - "querystring": "^0.2.0", - "svgsaver": "^0.9.0", - "urijs": "^1.19.10", - "xss": "^1.0.3" + "@ohdsi/atlascharts": "2.1.0", + "@ohdsi/ui-toolbox": "1.1.0", + "@ohdsi/visibilityjs": "2.0.2", + "ajv": "6.12.5", + "bootstrap": "3.4.1", + "bootstrap-select": "1.13.18", + "bowser": "2.11.0", + "clipboard": "2.0.11", + "colorbrewer": "1.5.3", + "crossfilter2": "1.5.4", + "d3": "4.13.0", + "d3-scale-chromatic": "1.5.0", + "d3-tip": "0.9.1", + "datatables": "1.10.18", + "datatables.net": "1.13.2", + "datatables.net-buttons": "2.3.4", + "datatables.net-select": "1.6.0", + "director": "1.2.8", + "eonasdan-bootstrap-datetimepicker": "4.17.49", + "express": "4.18.2", + "facets": "0.1.1", + "file-saver": "1.3.8", + "hash-it": "4.1.0", + "html2canvas": "1.4.1", + "jquery": "1.12.4", + "jquery-ui": "1.13.2", + "jquery-ui.autocomplete.scroll": "0.1.9", + "js-cookie": "2.2.0", + "papaparse": "5.3.2", + "jszip": "3.10.1", + "knockout": "3.5.1", + "knockout-mapping": "2.6.0", + "knockout-sortable": "1.2.2", + "leaflet": "1.9.3", + "less": "3.8.1", + "lodash": "4.17.21", + "lscache": "1.3.2", + "lz-string": "1.4.4", + "moment": "2.29.4", + "numeral": "2.0.6", + "ouanalyse-datatables.net-buttons-html5": "1.5.3", + "prismjs": "1.29.0", + "qs": "6.11.0", + "querystring": "0.2.1", + "svgsaver": "0.9.0", + "urijs": "1.19.11", + "venn.js": "0.2.20", + "xss": "1.0.14" } }