diff --git a/js/components/advancedSearch/advancedSearch.html b/js/components/advancedSearch/advancedSearch.html new file mode 100644 index 000000000..0c60f1adb --- /dev/null +++ b/js/components/advancedSearch/advancedSearch.html @@ -0,0 +1,61 @@ + + + + + + + +
+
+
+ +
+ + +
+
+
+
+ +
+
+ + +
+
+ \ No newline at end of file diff --git a/js/components/advancedSearch/advancedSearch.js b/js/components/advancedSearch/advancedSearch.js new file mode 100644 index 000000000..27a0fdcf9 --- /dev/null +++ b/js/components/advancedSearch/advancedSearch.js @@ -0,0 +1,99 @@ +define([ + 'knockout', + 'components/Component', + 'utils/AutoBind', + 'utils/CommonUtils', + 'text!./advancedSearch.html', + 'services/http', + 'pages/vocabulary/const', + './components/panelList', +], function ( + ko, + Component, + AutoBind, + commonUtils, + view, + httpService, + constants, +) { + class AdvancedSearch extends AutoBind(Component) { + constructor(params) { + super(params); + this.querySearch = ko.observable(''); + this.loading = ko.observable(false); + this.domains = ko.observable(); + this.selectedDomains = new Set(); + this.panelCollapsable = ko.observable(true); + this.searchConceptSets = params.searchConceptSets; + this.showSearch = params.showSearch; + this.showAdvanced = ko.observable(false); + this.refreshConceptSets = params.refreshConceptSets; + this.getDomains(); + + //subscriptions + this.showSearch.subscribe((newValue) => { + if(!newValue) { + this.panelCollapsable(true); + this.showAdvanced(false); + this.refreshConceptSets(); + } + }); + } + + clearAll(data,event) { + event.stopPropagation(); + $('.advanced-options input').attr('checked', false); + this.selectedDomains.clear(); + } + + selectDomain(id) { + this.selectedDomains.has(id) + ? this.selectedDomains.delete(id) + : this.selectedDomains.add(id, true); + } + + search() { + if (this.querySearch().trim().length < 2) { + return alert('Search must have at least 2 letters'); + } + const searchParams = { + query: this.querySearch, + domainId: Array.from(this.selectedDomains) + }; + this.showAdvanced(false); + this.panelCollapsable(true); + this.searchConceptSets(searchParams); + + } + + resetSearchConceptSets() { + $('.advanced-options input').attr('checked', false); + this.selectedDomains.clear(); + this.querySearch(''); + this.showAdvanced(false); + this.panelCollapsable(true); + this.refreshConceptSets(); + } + + getDomains() { + this.loading(true); + httpService.doGet(constants.apiPaths.domains()) + .then(({ data }) => { + this.domains(data); + }) + .catch(er => console.error('Error occured when loading domains', er)) + .finally(() => { + this.loading(false); + }); + } + + toggleAdvanced() { + if(this.showAdvanced()) { + this.panelCollapsable(true); + } + this.showAdvanced(!this.showAdvanced()); + } + + } + return commonUtils.build('advanced-search', AdvancedSearch, view); +}); diff --git a/js/components/advancedSearch/components/panel-list.less b/js/components/advancedSearch/components/panel-list.less new file mode 100644 index 000000000..b511b4f6b --- /dev/null +++ b/js/components/advancedSearch/components/panel-list.less @@ -0,0 +1,38 @@ +.panel { + + &__header { + font-size: 1.4rem; + padding-left: 1rem; + padding-right: 1rem; + } + + &__content { + padding: 1rem; + } +} + +.panel-heading-collapsible { + display: flex; + justify-content: space-between; + cursor: pointer; + border-top-right-radius: 4px; + border-top-left-radius: 4px; +} +.active .icon-chevron:after { + /* symbol for "collapsed" panels */ + font-family: 'Font Awesome 5 Free'; + content: "\f078"; + font-weight: 900; +} + +.icon-chevron:after { + font-family: 'Font Awesome 5 Free'; + content: "\f077"; + font-weight: 900; + +} + +.advanced-panel { + margin: 10px 0; + border-radius: 4px; +} diff --git a/js/components/advancedSearch/components/panelList.html b/js/components/advancedSearch/components/panelList.html new file mode 100644 index 000000000..18d33a8b1 --- /dev/null +++ b/js/components/advancedSearch/components/panelList.html @@ -0,0 +1,19 @@ +
+
+
+
+ +
+
+
+ +
+
+ + + + + diff --git a/js/components/advancedSearch/components/panelList.js b/js/components/advancedSearch/components/panelList.js new file mode 100644 index 000000000..f00d7b9a3 --- /dev/null +++ b/js/components/advancedSearch/components/panelList.js @@ -0,0 +1,29 @@ +define([ + 'knockout', + 'text!./panelList.html', + 'components/Component', + 'utils/CommonUtils', + 'less!./panel-list.less', +], function ( + ko, + view, + Component, + commonUtils +) { + class PanelList extends Component { + constructor(params) { + super(params); + this.title = params.title; + this.templateId = params.templateId; + this.context = params.context; + this.selectElement = params.selectElement; + this.showText = params.panelCollapsable; + } + + toggleShow() { + this.showText(!this.showText()); + } + } + + return commonUtils.build('panel-list', PanelList, view); +}); diff --git a/js/components/circe/components/ConceptSetBrowser.js b/js/components/circe/components/ConceptSetBrowser.js index db9df0830..5a2f0d6eb 100644 --- a/js/components/circe/components/ConceptSetBrowser.js +++ b/js/components/circe/components/ConceptSetBrowser.js @@ -7,10 +7,12 @@ define([ 'services/AuthAPI', 'utils/DatatableUtils', 'utils/CommonUtils', + 'services/ConceptSet', 'components/ac-access-denied', + 'components/advancedSearch/advancedSearch', 'databindings', 'css!./style.css' -], function (ko, template, VocabularyProvider, appConfig, ConceptSet, authApi, datatableUtils, commonUtils) { +], function (ko, template, VocabularyProvider, appConfig, ConceptSet, authApi, datatableUtils, commonUtils, conceptSetService) { function CohortConceptSetBrowser(params) { var self = this; @@ -59,7 +61,6 @@ define([ }); } - function setDisabledConceptSetButton(action) { if (action && action()) { return action() @@ -68,6 +69,12 @@ define([ } } + function prepareDataTable (results) { + datatableUtils.coalesceField(results, 'modifiedDate', 'createdDate'); + datatableUtils.addTagGroupsToFacets(results, self.options.Facets); + datatableUtils.addTagGroupsToColumns(results, self.columns); + } + self.datatableUtils = datatableUtils; self.criteriaContext = params.criteriaContext; self.cohortConceptSets = params.cohortConceptSets; @@ -96,12 +103,10 @@ define([ self.loadConceptSetsFromRepository = function (url) { self.loading(true); - - VocabularyProvider.getConceptSetList(url) + const sourceUrl = url ? url : self.selectedSource().url; + VocabularyProvider.getConceptSetList(sourceUrl) .done(function (results) { - datatableUtils.coalesceField(results, 'modifiedDate', 'createdDate'); - datatableUtils.addTagGroupsToFacets(results, self.options.Facets); - datatableUtils.addTagGroupsToColumns(results, self.columns); + prepareDataTable(results); self.repositoryConceptSets(results); self.loading(false); }) @@ -179,6 +184,35 @@ define([ const { pageLength, lengthMenu } = commonUtils.getTableOptions('M'); this.pageLength = params.pageLength || pageLength; this.lengthMenu = params.lengthMenu || lengthMenu; + + // advanced search + this.showSearch = ko.observable(false); + + self.checkSearchAvailable = async function () { + self.loading(true); + try { + const data = await conceptSetService.checkSearchAvailable(); + self.showSearch(data); + } catch(e) { + throw new Error(e); + } finally { + self.loading(false); + } + }; + self.searchConceptSets = async function (searchParams) { + self.loading(true); + try { + const data = await conceptSetService.searchConceptSets(searchParams); + + prepareDataTable(data); + self.repositoryConceptSets(data); + } catch(e) { + throw new Error(e); + } finally { + self.loading(false); + } + }; + self.checkSearchAvailable(); } var component = { diff --git a/js/components/circe/components/ConceptSetBrowserTemplate.html b/js/components/circe/components/ConceptSetBrowserTemplate.html index 07a5acbb8..00dd33eb7 100644 --- a/js/components/circe/components/ConceptSetBrowserTemplate.html +++ b/js/components/circe/components/ConceptSetBrowserTemplate.html @@ -7,9 +7,15 @@
-
+
+ +
-
-
-
-
+
+
+
+
+
-
-
-
+
+
+
- ( - ) + ( + ) +
+
+
+
+
+
+
+
+
+
+
@@ -135,6 +147,12 @@
+
+ +
diff --git a/js/pages/configuration/configuration.js b/js/pages/configuration/configuration.js index 76ac15225..1a95dd460 100644 --- a/js/pages/configuration/configuration.js +++ b/js/pages/configuration/configuration.js @@ -13,6 +13,7 @@ define([ 'services/Poll', 'services/job/jobDetail', 'services/CacheAPI', + 'services/ConceptSet', 'less!./configuration.less', 'components/heading' ], function ( @@ -30,6 +31,7 @@ define([ {PollService}, jobDetail, cacheApi, + conceptSetService ) { class Configuration extends AutoBind(Page) { constructor(params) { @@ -42,6 +44,7 @@ define([ this.jobListing = sharedState.jobListing; this.sourceJobs = new Map(); this.sources = sharedState.sources; + this.reindexJob = ko.observable(); this.priorityOptions = [ {id: 'session', name: ko.i18n('configuration.priorityOptions.session', 'Current Session')}, @@ -80,9 +83,45 @@ define([ }); this.intervalId = PollService.add({ - callback: () => this.checkJobs(), + callback: () => { + this.checkJobs(); + this.checkReindexJob(); + }, interval: config.pollInterval }); + + this.searchAvailable = ko.observable(false); + + this.checkSearchAvailable(); + } + + async checkSearchAvailable () { + this.loading(true); + try { + const data = await conceptSetService.checkSearchAvailable(); + this.searchAvailable(data); + } catch(e) { + throw new Error(e); + } finally { + this.loading(false); + } + } + + async reindexConceptSets() { + const confirmAction = confirm(ko.unwrap(ko.i18n('configuration.confirms.reindexSource', 'Reindexing may take a long time. It depends on amount and complexity of concept sets'))); + if (!confirmAction) { + return; + } + try { + const data =await conceptSetService.reindexConceptSets(); + if (data.status === 'RUNNING') { + alert(ko.unwrap(ko.i18n('configuration.alerts.reindexRunning', 'Reindexing of concept sets is currently in progress'))); + } else { + this.reindexJob(data); + } + } catch(e) { + throw new Error(e); + } } dispose() { @@ -111,6 +150,21 @@ define([ }); } + async checkReindexJob() { + try { + let data; + if (this.reindexJob()) { + data = await conceptSetService.statusReindexConceptSets(this.reindexJob().executionId); + } else { + data = await conceptSetService.statusReindexConceptSets(); + } + this.reindexJob(data); + } catch(e) { + this.reindexJob(null); + throw new Error(e); + } + } + async onPageCreated() { this.loading(true); await sourceApi.initSourcesConfig(); @@ -186,6 +240,9 @@ define([ var selectedSource = sharedState.sources().find((item) => { return item.vocabularyUrl === newVocabUrl; }); sharedState.priorityScope() === 'application' && sharedState.defaultVocabularyUrl(newVocabUrl); this.updateSourceDaimonPriority(selectedSource.sourceKey, 'Vocabulary'); + if (this.searchAvailable()) { + alert(ko.unwrap(ko.i18n('configuration.alerts.changeSource', 'You are changing current source, we recommend you to do reindexing concept sets'))); + } return true; }; @@ -238,6 +295,36 @@ define([ return this.getButtonStyles(source.connectionCheck()); } + getReindexButtonStyles() { + let iconClass = 'fa-caret-right'; + let buttonClass = 'btn-primary'; + let state = sourceApi.buttonCheckState.unknown; + if (this.reindexJob()) { + switch(this.reindexJob().status) { + case 'COMPLETED': + state = sourceApi.buttonCheckState.success; + break; + case 'FAILED': + state = sourceApi.buttonCheckState.failed; + break; + case 'CREATED': + case 'RUNNING': + state = sourceApi.buttonCheckState.checking; + break; + } + } + return this.getButtonStyles(state); + } + + getReindexButtonTitle() { + if (this.reindexJob() && this.reindexJob().status !== 'UNAVAILABLE') { + return ko.unwrap(ko.i18nformat('configuration.buttons.reindexCSStatus', 'Concept Sets Reindex (<%=doneCount%> of <%=maxCount%>)', + {doneCount: this.reindexJob().doneCount, maxCount: this.reindexJob().maxCount})()); + } else { + return ko.unwrap(ko.i18n('configuration.buttons.reindexCS', 'Concept Sets Reindex')); + } + } + getButtonStyles(sourceState) { let iconClass = 'fa-caret-right'; let buttonClass = 'btn-primary'; diff --git a/js/pages/configuration/configuration.less b/js/pages/configuration/configuration.less index 0567e502b..9a9ef7dae 100644 --- a/js/pages/configuration/configuration.less +++ b/js/pages/configuration/configuration.less @@ -16,4 +16,17 @@ &__manage-btn { width: 175px; } +} + +.status-container { + display: flex; + flex-direction: column; + align-items: flex-start; +} +.status { + display: flex; + margin-bottom: 8px; + :last-child { + margin-bottom: 0; + } } \ No newline at end of file diff --git a/js/services/ConceptSet.js b/js/services/ConceptSet.js index 435d050db..43e187fc1 100644 --- a/js/services/ConceptSet.js +++ b/js/services/ConceptSet.js @@ -29,6 +29,25 @@ define(function (require) { alert(message); self.isLoading(false); }); + }; + + function searchConceptSets(searchParams) { + const sourceKey = sharedState.sourceKeyOfVocabUrl(); + return httpService.doPost(`${config.api.url}conceptset/${sourceKey}/search`, searchParams).then(({ data }) => data); + } + + function checkSearchAvailable() { + return httpService.doGet(config.api.url + 'conceptset/searchAvailable').then(({ data }) => data); + } + + function reindexConceptSets() { + const sourceKey = sharedState.sourceKeyOfVocabUrl(); + return httpService.doGet(`${config.api.url}conceptset/${sourceKey}/index`).then(({ data }) => data); + } + + function statusReindexConceptSets(jobExecutionId) { + const sourceKey = sharedState.sourceKeyOfVocabUrl(); + return httpService.doGet(`${config.api.url}conceptset/${sourceKey}/index/${jobExecutionId || -1}/status`).then(({ data }) => data); } function lookupIdentifiers(identifiers) { @@ -140,7 +159,11 @@ define(function (require) { getVersion, getVersionExpression, updateVersion, - copyVersion + copyVersion, + searchConceptSets, + checkSearchAvailable, + reindexConceptSets, + statusReindexConceptSets }; return api; diff --git a/js/styles/atlas.css b/js/styles/atlas.css index 6e314e9ac..55fd96047 100644 --- a/js/styles/atlas.css +++ b/js/styles/atlas.css @@ -601,7 +601,6 @@ th { .configureHeader .left{ display: flex; - align-items: center; box-sizing: border-box; } diff --git a/js/utils/DatatableUtils.js b/js/utils/DatatableUtils.js index bb39cdc57..525fd1b17 100644 --- a/js/utils/DatatableUtils.js +++ b/js/utils/DatatableUtils.js @@ -83,7 +83,11 @@ define(['knockout', 'services/MomentAPI', 'xss', 'appConfig', 'services/AuthAPI' const addTagGroupsToFacets = (list, facets) => { extractTagGroups(list).sort().reverse().forEach(tg => { + if (facets.filter(f => f.isTagGroup && f.caption === tg.name).length > 0) { // do not add facet if it is already there + return; + } facets.unshift({ + isTagGroup: true, caption: tg.name, binding: (o) => { let tags = o.tags && o.tags.length > 0 @@ -100,7 +104,11 @@ define(['knockout', 'services/MomentAPI', 'xss', 'appConfig', 'services/AuthAPI' const addTagGroupsToColumns = (list, columns) => { extractTagGroups(list).forEach(tg => { + if (ko.unwrap(columns).filter(c => c.isTagGroup && c.title === tg.name).length > 0) { // do not add column if it is already there + return; + } columns.push({ + isTagGroup: true, title: tg.name, width: '100px', // default width visible: !!tg.showGroup,