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