diff --git a/js/pages/main.js b/js/pages/main.js
index 5dffd9f6e..d53917389 100644
--- a/js/pages/main.js
+++ b/js/pages/main.js
@@ -16,6 +16,7 @@ define(
const jobs = require('./jobs/index');
const configuration = require('./configuration/index');
const feedback = require('./feedback/index');
+ const tools = require('./tools/index');
// order of nav items in left-nav will appear in the following order:
return {
@@ -35,6 +36,7 @@ define(
jobs,
configuration,
feedback,
+ tools
};
}
);
\ No newline at end of file
diff --git a/js/pages/tools/PermissionService.js b/js/pages/tools/PermissionService.js
new file mode 100644
index 000000000..008825f3f
--- /dev/null
+++ b/js/pages/tools/PermissionService.js
@@ -0,0 +1,24 @@
+define([
+ 'services/AuthAPI',
+], function (
+ AuthAPI,
+) {
+ return class PermissionService {
+
+ static isPermittedReadTools() {
+ return AuthAPI.isPermitted('tool:get');
+ }
+
+ static isPermittedCreateTool() {
+ return AuthAPI.isPermitted('tool:post');
+ }
+
+ static isPermittedUpdateTool() {
+ return AuthAPI.isPermitted('tool:put');
+ }
+
+ static isPermittedDeleteTool() {
+ return AuthAPI.isPermitted('tool:*:delete');
+ }
+ }
+});
\ No newline at end of file
diff --git a/js/pages/tools/const.js b/js/pages/tools/const.js
new file mode 100644
index 000000000..fd99a7f56
--- /dev/null
+++ b/js/pages/tools/const.js
@@ -0,0 +1,9 @@
+define(
+ (require, exports) => {
+ const pageTitle = 'Tools';
+
+ return {
+ pageTitle,
+ };
+ }
+);
\ No newline at end of file
diff --git a/js/pages/tools/index.js b/js/pages/tools/index.js
new file mode 100644
index 000000000..a6e79cf80
--- /dev/null
+++ b/js/pages/tools/index.js
@@ -0,0 +1,14 @@
+define(
+ (require, exports) => {
+ const ko = require('knockout');
+ const buildRoutes = require('./routes');
+
+ return {
+ title: ko.i18n('navigation.tools', 'Tools'),
+ buildRoutes,
+ navUrl: () => '#/tools',
+ icon: 'toolbox',
+ statusCss: () => ""
+ };
+ }
+);
\ No newline at end of file
diff --git a/js/pages/tools/routes.js b/js/pages/tools/routes.js
new file mode 100644
index 000000000..8826ad7ce
--- /dev/null
+++ b/js/pages/tools/routes.js
@@ -0,0 +1,16 @@
+define(
+ (require, factory) => {
+ const { AuthorizedRoute } = require('pages/Route');
+ function routes(router) {
+ return {
+ '/tools': new AuthorizedRoute(() => {
+ require(['pages/tools/tool-manager'], function () {
+ router.setCurrentView('tool-manager');
+ });
+ }),
+ };
+ }
+
+ return routes;
+ }
+);
\ No newline at end of file
diff --git a/js/pages/tools/tool-management.less b/js/pages/tools/tool-management.less
new file mode 100644
index 000000000..86899d9de
--- /dev/null
+++ b/js/pages/tools/tool-management.less
@@ -0,0 +1,16 @@
+.modal-footer {
+ display: table;
+ width: 100%;
+ padding: 0;
+ border: 0;
+ margin-top: 18px;
+
+ .modal-buttons {
+ display: table-cell;
+ text-align: right;
+
+ button + button {
+ margin-left: 10px !important;
+ }
+ }
+}
\ No newline at end of file
diff --git a/js/pages/tools/tool-manager.html b/js/pages/tools/tool-manager.html
new file mode 100644
index 000000000..522818d30
--- /dev/null
+++ b/js/pages/tools/tool-manager.html
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/js/pages/tools/tool-manager.js b/js/pages/tools/tool-manager.js
new file mode 100644
index 000000000..9d499e69e
--- /dev/null
+++ b/js/pages/tools/tool-manager.js
@@ -0,0 +1,191 @@
+define([
+ 'knockout',
+ 'text!./tool-manager.html',
+ 'pages/Page',
+ 'utils/CommonUtils',
+ 'services/AuthAPI',
+ 'services/ToolService',
+ 'services/MomentAPI',
+ './PermissionService',
+ 'css!styles/switch-button.css',
+ 'less!./tool-management.less',
+], function (
+ ko,
+ view,
+ Page,
+ commonUtils,
+ authApi,
+ toolService,
+ momentApi,
+ PermissionService
+) {
+ class ToolManage extends Page {
+ constructor(params) {
+ super(params);
+ this.data_tools = ko.observableArray();
+ this.loading = ko.observable();
+ this.showModalAddTool = ko.observable(false);
+ this.newName = ko.observable(null);
+ this.newUrl = ko.observable(null);
+ this.newDescription = ko.observable(null);
+ this.toolIsVisible = ko.observable(false);
+
+ this.getValueUrl = this.getValueUrl.bind(this);
+ this.handleDataAddTool = this.handleDataAddTool.bind(this);
+ this.handleOpenLink = this.handleOpenLink.bind(this);
+ this.handleCancelTool = this.handleCancelTool.bind(this);
+ this.handleClearData = this.handleClearData.bind(this);
+ this.isAuthenticated = authApi.isAuthenticated;
+
+ this.canReadTools = PermissionService.isPermittedReadTools;
+ this.canCreateTool = PermissionService.isPermittedCreateTool;
+ this.canUpdateTool = PermissionService.isPermittedUpdateTool;
+ this.canDeleteTool = PermissionService.isPermittedDeleteTool;
+
+ this.showModalAddTool.subscribe((isShow) => {
+ if(!isShow) this.handleClearData();
+ })
+ }
+
+
+ handleIsEditing(id) {
+ const newData = this.data_tools().map((item) => {
+ if(id === item.id){
+ return ({
+ ...item,
+ isEditing: true
+ })
+ }
+ return item
+ })
+ this.data_tools(newData);
+ }
+
+ handleClearData(){
+ this.newName(null);
+ this.newDescription(null);
+ this.newUrl(null);
+ this.toolIsVisible(false);
+ this.showModalAddTool(false);
+ }
+
+ handleCancelTool(){
+ this.handleClearData();
+ }
+
+ handleOpenLink(data, event){
+ if(data.isEditing) return;
+ if(["editIcon", "deleteIcon", "completeIcon"].includes(event.target.id)) return;
+ return window.open(data.url, '_blank');
+ }
+
+ async handleIsEdited(id) {
+ this.loading(true);
+ try {
+ const dataAdjust = this.data_tools().find(tool => tool.id === id);
+ const data = {
+ id,
+ name: dataAdjust.name,
+ url: dataAdjust.url,
+ description: dataAdjust.description,
+ enabled: dataAdjust.enabled
+ }
+ await toolService.updateTool(data);
+ }catch(error){
+ console.log('update tool failed', error)
+ } finally {
+ this.loading(false);
+ this.getToolFromAllPages();
+ return false;
+ }
+ }
+
+ async handleDelete(id) {
+ this.loading(true);
+ try {
+ await toolService.deleteTool(id);
+ }catch(error){
+ console.log('delete tool failed', error)
+ } finally {
+ this.loading(false);
+ this.getToolFromAllPages();
+ }
+ }
+
+ async toggleVisiableTool(){
+ this.toolIsVisible(!this.toolIsVisible())
+ }
+
+ async handleCheckVisible(id){
+ const newData = this.data_tools().map((item) => {
+ if(id === item.id){
+ return ({
+ ...item,
+ enabled: !item.enabled
+ })
+ }
+ return item
+ })
+ await this.data_tools(newData);
+ }
+
+ getValueUrl(data, event){
+ const newData = this.data_tools().map((item) => {
+ if(data.id === item.id){
+ return ({
+ ...item,
+ url: data.url,
+ description: data.description
+ })
+ }
+ return item
+ })
+ this.data_tools(newData);
+ }
+
+ async handleDataAddTool(data){
+ this.loading(true);
+ try {
+ const dataPayload = {
+ name: data.newName(),
+ description: data.newDescription(),
+ url: data.newUrl(),
+ enabled: data.toolIsVisible()
+ };
+ await toolService.createTool(dataPayload);
+ }catch(error){
+ console.log('add new tool failed', error)
+ } finally {
+ this.handleClearData();
+ this.getToolFromAllPages();
+ this.loading(false);
+ }
+ }
+
+ async onPageCreated() {
+ if(this.canReadTools()){
+ this.getToolFromAllPages();
+ }
+ }
+
+ async getToolFromAllPages() {
+ this.loading(true);
+ try {
+ this.data_tools([]);
+ const dataTools = await toolService.getTools();
+ Array.isArray(dataTools) && dataTools.forEach((item, index) => {
+ this.data_tools.push({
+ ...item,
+ createdDate: momentApi.formatDate(item.createdDate, 'DD/MM/YYYY HH:mm:ss'),
+ isEditing: false,
+ updatedDate: momentApi.formatDate(item.modifiedDate, 'DD/MM/YYYY HH:mm:ss'),
+ createdBy: item.createdByName ?? ''
+ })});
+ } finally {
+ this.loading(false);
+ }
+ }
+ }
+
+ return commonUtils.build('tool-manager', ToolManage, view);
+});
diff --git a/js/services/ToolService.js b/js/services/ToolService.js
new file mode 100644
index 000000000..d73219839
--- /dev/null
+++ b/js/services/ToolService.js
@@ -0,0 +1,44 @@
+define([
+ 'appConfig',
+ 'services/http'],
+function(
+ appConfig,
+ httpService
+) {
+ const ko = require('knockout');
+
+ function getTools() {
+ return httpService.doGet(appConfig.webAPIRoot + 'tool').then(({ data }) => data)
+ .catch((err) => {
+ alert(ko.i18n('tool.error.list', 'Get list tool error, try later !')());
+ });
+ }
+
+ function updateTool(tool) {
+ return httpService.doPut(appConfig.webAPIRoot + 'tool', tool)
+ .catch((err) => {
+ alert(ko.i18n('tool.error.update', 'Update tool error, try later !')());
+ });
+ }
+
+ function createTool(tool){
+ return httpService.doPost(appConfig.webAPIRoot + 'tool', tool)
+ .catch((err) => {
+ alert(ko.i18n('tool.error.create', 'Create tool error, try later !')());
+ });
+ }
+
+ function deleteTool(id){
+ return httpService.doDelete(appConfig.webAPIRoot + `tool/${id}`)
+ .catch((err) => {
+ alert(ko.i18n('tool.error.delete', 'Delete tool error, try later !')());
+ });
+ }
+
+ return {
+ getTools,
+ updateTool,
+ createTool,
+ deleteTool
+ }
+});
\ No newline at end of file
diff --git a/js/settings.js b/js/settings.js
index ec02fb064..123afcc28 100644
--- a/js/settings.js
+++ b/js/settings.js
@@ -170,7 +170,8 @@ const settingsObject = {
"jquery.dataTables.colVis.css": "css!styles/jquery.dataTables.colVis.css",
"jquery.datatables.tabletools.css": "css!styles/jquery.datatables.tabletools.css",
"prism.css": "css!styles/prism.css",
- "leaflet": "css!../node_modules/leaflet/dist/leaflet.css"
+ "leaflet": "css!../node_modules/leaflet/dist/leaflet.css",
+ "switch-button.css": "css!styles/switch-button.css"
},
localRefs: {
"configuration": "components/configuration",
diff --git a/js/styles/atlas.css b/js/styles/atlas.css
index 6e314e9ac..6155cb390 100644
--- a/js/styles/atlas.css
+++ b/js/styles/atlas.css
@@ -2179,3 +2179,65 @@ div.wrapper_ohdsi a {
padding: 1rem;
padding-bottom: 0;
}
+
+.card-tools {
+ top: 30px;
+ position: relative;
+}
+
+.add-btn {
+ position: absolute;
+ width: 180px;
+ top: 10px;
+ right: 20px;
+}
+
+.bg-add {
+ background-color: #0070dd;
+}
+
+.card-tool-container {
+ display: flex;
+ flex-wrap: wrap;
+ position: absolute;
+ margin-top: 70px;
+ margin-left: 20px;
+ margin-right: 30px;
+ gap: 20px;
+}
+
+.card-tool {
+ border: 2.4px solid #000;
+ border-radius: 10px;
+ width: 400px;
+ max-width: 410px;
+ padding: 20px;
+ transition: all 0.2s ease-out;
+}
+
+.card-tool:hover{
+ box-shadow: 2px 5px 12px #bebebe,
+ -2px -5px 12px #bebebe;
+}
+
+.card-tool h3{
+ margin-top: 0px;
+ margin-bottom: 0px;
+ font-weight: 650;
+ color: #203D54;
+ text-decoration: none;
+ cursor: pointer;
+ font-size: 26px;
+}
+
+.flex-between{
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.flex-end{
+ display: flex;
+ justify-content: flex-end;
+}
\ No newline at end of file
diff --git a/js/styles/switch-button.css b/js/styles/switch-button.css
new file mode 100644
index 000000000..e876485fa
--- /dev/null
+++ b/js/styles/switch-button.css
@@ -0,0 +1,47 @@
+.material-switch > input[type="checkbox"] {
+ display: none;
+}
+
+.material-switch > label {
+ cursor: pointer;
+ height: 0px;
+ position: relative;
+ width: 40px;
+}
+
+.material-switch > label::before {
+ background: rgb(0, 0, 0);
+ box-shadow: inset 0px 0px 10px rgba(0, 0, 0, 0.5);
+ border-radius: 8px;
+ content: '';
+ height: 16px;
+ margin-top: -8px;
+ position:absolute;
+ opacity: 0.3;
+ transition: all 0.4s ease-in-out;
+ width: 40px;
+}
+
+.material-switch > label::after {
+ background: rgb(255, 255, 255);
+ border-radius: 16px;
+ box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
+ content: '';
+ height: 24px;
+ left: -4px;
+ margin-top: -8px;
+ position: absolute;
+ top: -4px;
+ transition: all 0.3s ease-in-out;
+ width: 24px;
+}
+
+.material-switch > .label-switch-enable::before {
+ background: inherit;
+ opacity: 0.5;
+}
+
+.material-switch > .label-switch-enable::after {
+ background: inherit;
+ left: 20px;
+}
\ No newline at end of file