Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions js/components/authorship.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,20 @@
<span data-bind="visible: createdBy,
text: ko.i18nformat('components.authorship.createdLabel',
'created by <%=createdBy%> on <%=cratedDate%>',
{createdBy: createdBy, createdDate: createdDate})"></span><span data-bind="visible: modifiedDate">,</span>
{createdBy: createdBy, createdDate: createdDate})">
</span>
<span data-bind="visible: modifiedDate">,</span>
<span data-bind="visible: modifiedDate,
text: ko.i18nformat('components.authorship.modifiedLabel',
'modified by <%=modifiedBy%> on <%=modifiedDate%>',
{modifiedBy: modifiedBy, modifiedDate: modifiedDate})"></span>
{modifiedBy: modifiedBy, modifiedDate: modifiedDate})">
</span>
<span data-bind="visible: lockedDate">,</span>
<span data-bind="visible: lockedDate">
<span data-bind="text: ko.i18n('components.authorship.locked', 'locked')"></span>
<span data-bind="visible: lockedBy, text: ko.i18nformat('components.authorship.byLocked', 'by <%=lockedBy%>', {lockedBy: lockedBy})"></span>
<span data-bind="text: ko.i18n('components.authorship.on', 'on')"></span>
<span data-bind="text: lockedDate"></span>
</span>
</div>
<!-- /ko -->
2 changes: 2 additions & 0 deletions js/components/authorship.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ define([
this.createdDate = params.createdDate;
this.modifiedBy = params.modifiedBy;
this.modifiedDate = params.modifiedDate;
this.lockedBy = params.lockedBy;
this.lockedDate = params.lockedDate;
}
}

Expand Down
47 changes: 41 additions & 6 deletions js/components/circe/components/ConceptSetBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ define([
'services/AuthAPI',
'utils/DatatableUtils',
'utils/CommonUtils',
'services/ConceptSet',
'components/ac-access-denied',
'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;

Expand Down Expand Up @@ -96,17 +97,42 @@ define([

self.loadConceptSetsFromRepository = function (url) {
self.loading(true);

VocabularyProvider.getConceptSetList(url)
.done(function (results) {
datatableUtils.coalesceField(results, 'modifiedDate', 'createdDate');
datatableUtils.addTagGroupsToFacets(results, self.options.Facets);
datatableUtils.addTagGroupsToColumns(results, self.columns);
self.repositoryConceptSets(results);
self.loading(false);

// Extract IDs for batch locked status check
const conceptSetIds = results.map(cs => cs.id);
const isLockedBatchCheckRequest = {
conceptSetIds: conceptSetIds
};
conceptSetService.getLockedStatusesForConceptSets(isLockedBatchCheckRequest).then(response => {
const lockStatusMap = response.data.lockStatus;
// Apply the locking status to each ConceptSet
results.forEach(conceptSet => {
conceptSet.isLocked = lockStatusMap[conceptSet.id] || false;
});
// Update the observable array with locked statuses
self.repositoryConceptSets(results);
datatableUtils.addTagGroupsToColumns(results, self.columns);
self.loading(false);
}).catch(error => {
console.error('Error while batch-fetching locked statuses for concept sets', error);
// Defaulting isLocked to false in case of error
results.forEach(conceptSet => {
conceptSet.isLocked = false;
});
self.repositoryConceptSets(results);
datatableUtils.addTagGroupsToColumns(results, self.columns);
self.loading(false);
});

})
.fail(function (err) {
console.log(err);
console.log('Error fetching concept sets:', err);
self.loading(false);
});
}

Expand Down Expand Up @@ -150,6 +176,15 @@ define([
};

this.columns = ko.observableArray([
{
title: '',
data: 'isLocked',
sortable: false,
render: function (data, type, row) {
return data ? '<span class="fa fa-key" aria-hidden="true""></span>' : '';
},
width: '20px',
},
{
title: ko.i18n('columns.id', 'Id'),
data: 'id'
Expand Down
9 changes: 9 additions & 0 deletions js/components/circe/components/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,12 @@
.conceptset-browser-panel .new-concept-set {
width: 175px;
}

.fa-key {
color: yellow !important;
text-shadow:
-0.5px -0.5px 0 black,
0.5px -0.5px 0 black,
-0.5px 0.5px 0 black,
0.5px 0.5px 0 black;
}
3 changes: 2 additions & 1 deletion js/components/conceptset/const.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ define([
EXPORT: 'conceptset-export',
IMPORT: 'conceptset-import',
ANNOTATION: 'annotation',
MAPPINGS: 'resolve-mappings'
MAPPINGS: 'resolve-mappings',
LOCK_HISTORY: 'lockHistory',
};

const ConceptSetSources = {
Expand Down
91 changes: 91 additions & 0 deletions js/pages/concept-sets/components/modal/snapshot-lock-modal.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<atlas-modal
params="data: $component, showModal: $component.isModalShown, title: isLocked() ? ko.i18n('components.unlock.title', 'Unlock Concept Set') : ko.i18n('components.snapshot.title', 'Snapshot Actions')">
<div class="modal-body" data-bind="visible: true">
<!-- Unlocked Concept Set Content -->
<div data-bind="visible: !isLocked()">
<p class="modal-text"><strong>You are about to Lock and/or create a snapshot for this concept set.</strong>
</p>
<br>
<p> <i class="fas fa-info-circle"></i><strong>Locking a concept set means that the Concept Set
Expression tab cannot be edited anymore. However, vocabulary updates can still add new codes
that are mapped to the existing codes in the Concept Set Expression.</strong><br>
<i class="fas fa-info-circle"></i><strong>A snapshot saves a static version of the Concept Set
Expression, Included Concepts, and Included Source Codes tabs at the time it is created. If the
Concept Set is later modified, the snapshot remains unchanged. Additionally, if the Concept Set
is affected by a vocabulary update, the snapshot will not be altered.</strong>
</p><br>
<p class="modal-text">
There are two options: <strong>Snapshot</strong> or a <strong>Snapshot + Lock</strong>.
<br><br><strong>Plain snapshot:</strong>
</p>
<ul>
<li>Snapshot saves a static version of the concept set but does not prevent modification to the Concept
Set Expression tab.</li>
<li>The Snapshot saves the Concept set expression, Included Concepts and Included Source Codes.</li>
<li>Snapshot saves current vocabulary version and schema will be saved</li>
<li>Snapshots will not be impacted by future vocabulary updates</li>
<li>Current SYSTEM vocabulary version: <strong data-bind="text: currentVocabularyVersion"></strong></li>
<li>Current SYSTEM vocabulary schema: <strong data-bind="text: currentVocabularySchema"></strong></li>
</ul>
<br><strong>Snapshot with Lock (along with the above actions):</strong><br>
<ul>
<li>Concept Set will be switched to read-only mode and marked locked.</li>
</ul>
<br>
<p class="modal-text">This action might take some time and would continue even if this window is closed
</p>
<p class="modal-text" style="margin-top: 20px;"><strong>Snapshot description message<span
style="color: red;">*</span>:</strong></p>
<input type="text" rows="4" data-bind="textInput: snapshotDescriptionMessage,
css: classes({
element: 'input',
extra: 'form-control'
}),
placeholder: ko.i18n('snapshot.description.placeholder', 'Type your snapshot description here')" />

<div class="action-status" data-bind="visible: actionInProgress, text: actionInProgress"></div>
<div class="center-buttons">
<button class="btn btn-success btn-sm"
data-bind="click: snapshotAndLock, enable: canExecuteActions() && !isLocking() && !isUnlocking() && !isSnapshotting()">Snapshot
+ Lock</button>
<button class="btn btn-success btn-sm"
data-bind="click: snapshotOnly, enable: canExecuteActions() && !isLocking() && !isUnlocking() && !isSnapshotting()">Snapshot
Only</button>
<button class="btn btn-danger btn-sm" data-bind="click: () => isModalShown(false)">Cancel</button>
</div>
</div>

<!-- Locked Concept Set Content -->
<div data-bind="visible: isLocked()">
<p class="modal-text">You are about to UNLOCK this concept set.</p>
<p class="modal-text">
The following will happen:
<ul>
<li>Concept Set read-only mode will be disabled</li>
<li>Optional: Take a snapshot after unlock</li>
</ul>
</p>
<div class="checkbox-container">
<input id="snapshotCheckbox" name="snapshotCheckbox" type="checkbox"
data-bind="checked: takeSnapshotWhenUnlocking, enable: true" />
<label for="snapshotCheckbox" style="font-size: 13px; align-items: center;">Take snapshot</label>
</div>
<br>
<p class="modal-text">This action might take some time and would continue even if this window is closed
</p>
<p class="modal-text" style="margin-top: 20px;"><strong>Unlock confirmation message<span
style="color: red;">*</span>:</strong></p>
<textarea class="form-control large-text-input"
data-bind="textInput: snapshotDescriptionMessage, enable: true" rows="4"></textarea>
<div class="action-status" data-bind="visible: actionInProgress, text: actionInProgress"></div>
<div class="center-buttons">
<button class="btn btn-success btn-sm"
data-bind="click: () => unlockConceptSet(takeSnapshotWhenUnlocking()), enable: canExecuteActions()"
style="background-color: green">Confirm</button>
<button class="btn btn-primary btn-sm"
data-bind="click: () => isModalShown(false) && !isLocking() && !isUnlocking() && !isSnapshotting()"
style="background-color: blue">Cancel</button>
</div>
</div>
</div>
</atlas-modal>
144 changes: 144 additions & 0 deletions js/pages/concept-sets/components/modal/snapshot-lock-modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
define([
'knockout',
'text!./snapshot-lock-modal.html',
'components/Component',
'utils/CommonUtils',
'utils/AutoBind',
'services/SourceAPI',
'services/ConceptSet',
'atlas-state',
'less!./snapshot-lock-modal.less',
'databindings',
], function (
ko,
view,
Component,
commonUtils,
AutoBind,
sourceApi,
conceptSetService,
sharedState,
) {
class SnapshotLockModal extends AutoBind(Component) {
constructor(params) {
super(params);
this.isModalShown = params.isModalShown;
this.isLocked = params.isLocked;
this.conceptSetStore = params.conceptSetStore;
this.parentLoading = params.parentLoading;

this.isLocking = ko.observable(false);
this.isUnlocking = ko.observable(false);
this.isSnapshotting = ko.observable(false);
this.actionInProgress = ko.observable('');

this.takeSnapshotWhenUnlocking = ko.observable(false);
this.currentConceptSetId = params.currentConceptSetId;
this.currentVocabularyVersion = params.currentVocabularyVersion;
this.currentVocabularySchema = ko.observable();
this.snapshotDescriptionMessage = ko.observable('');
this.canExecuteActions = ko.pureComputed(() => {
const hasSnapshotDesc = this.snapshotDescriptionMessage().trim().length > 0;
return hasSnapshotDesc;
});
this.fetchAndSetVocabularySchema();
}

async fetchAndSetVocabularySchema() {
try {
const source = await sourceApi.getSourceInfo(sharedState.sourceKeyOfVocabUrl());
this.processSourceData(source);
} catch (error) {
console.error(`Error fetching source information: ${error}`);
this.currentVocabularySchema(undefined);
}
}

processSourceData(source) {
if (!source || !source.daimons) {
this.currentVocabularySchema(undefined);
} else {
const vocabularyDaimon = source.daimons.find(daimon => daimon.daimonType === 'Vocabulary');
this.currentVocabularySchema(vocabularyDaimon ? vocabularyDaimon.tableQualifier : undefined);
}
}


createSnapshotActionRequest(action, takeSnapshot = true) {
return {
sourceKey: sharedState.sourceKeyOfVocabUrl(),
action: action,
message: this.snapshotDescriptionMessage(),
takeSnapshot: takeSnapshot
};
}

snapshotAndLock() {
if (this.isLocking()) return;
this.isLocking(true);
this.parentLoading(true);
this.actionInProgress('Locking and creating a snapshot...');

const request = this.createSnapshotActionRequest("LOCK");
conceptSetService.invokeConceptSetSnapshotAction(this.currentConceptSetId(), request)
.then(() => {
this.conceptSetStore.current.notifySubscribers();
this.snapshotDescriptionMessage("");
this.isModalShown(false);
console.log("Concept set locked and snapshot created");
})
.catch(error => console.error(`Error locking concept set: ${error}`))
.finally(() => {
this.isLocking(false);
this.parentLoading(false);
this.actionInProgress('');
});
}

snapshotOnly() {
if (this.isSnapshotting()) return;
this.isSnapshotting(true);
this.parentLoading(true);
this.actionInProgress('Creating a snapshot...');


const request = this.createSnapshotActionRequest("SNAPSHOT");
conceptSetService.invokeConceptSetSnapshotAction(this.currentConceptSetId(), request)
.then(() => {
this.snapshotDescriptionMessage("");
console.log("Concept set snapshot created");
this.isModalShown(false);
})
.catch(error => console.error(`Error creating snapshot: ${error}`))
.finally(() => {
this.isSnapshotting(false);
this.parentLoading(false);
this.actionInProgress('');
});
}

unlockConceptSet(takeSnapshot) {
if (this.isUnlocking()) return;
this.isUnlocking(true);
this.parentLoading(true);
this.actionInProgress('Unlocking ...');

const request = this.createSnapshotActionRequest("UNLOCK", takeSnapshot);
conceptSetService.invokeConceptSetSnapshotAction(this.currentConceptSetId(), request)
.then(() => {
this.conceptSetStore.current.notifySubscribers();
this.snapshotDescriptionMessage("");
this.isModalShown(false);
console.log("Concept set unlocked");
})
.catch(error => console.error(`Error unlocking concept set: ${error}`))
.finally(() => {
this.isUnlocking(false);
this.parentLoading(false);
this.actionInProgress('');
});
}
}

return commonUtils.build('snapshot-lock-modal', SnapshotLockModal, view);
});
Loading