From d52a8c1f0585a4632f5cc796395713138f998c30 Mon Sep 17 00:00:00 2001 From: darta Date: Tue, 28 Dec 2021 10:32:14 +0100 Subject: [PATCH] feat(admin): added customization dashboard --- admin/docker/Dockerfile | 2 +- admin/docker/requirements.pip3 | 1 + admin/src/admin/lib/dashboard.py | 88 ++++ admin/src/admin/static/dd.css | 14 +- admin/src/admin/static/js/dashboard.js | 443 ++++++++++++++++++ admin/src/admin/static/templates/base.html | 2 +- admin/src/admin/static/templates/header.html | 1 + .../static/templates/pages/dashboard.html | 433 +++++++++++++++++ admin/src/admin/static/templates/sidebar.html | 4 +- admin/src/admin/views/ApiViews.py | 60 ++- admin/src/admin/views/WebViews.py | 11 + admin/src/scripts/parse_fontawesome-icons.py | 24 + docker-compose-parts/admin.yml | 2 +- 13 files changed, 1052 insertions(+), 33 deletions(-) create mode 100644 admin/src/admin/lib/dashboard.py create mode 100644 admin/src/admin/static/js/dashboard.js create mode 100644 admin/src/admin/static/templates/pages/dashboard.html create mode 100644 admin/src/scripts/parse_fontawesome-icons.py diff --git a/admin/docker/Dockerfile b/admin/docker/Dockerfile index 18bd608..9a3af66 100644 --- a/admin/docker/Dockerfile +++ b/admin/docker/Dockerfile @@ -12,7 +12,7 @@ COPY admin/docker/requirements.pip3 /requirements.pip3 RUN pip3 install --no-cache-dir -r requirements.pip3 RUN apk del .build_deps -RUN apk add --no-cache curl py3-yaml yarn libpq openssl +RUN apk add --no-cache curl py3-yaml yarn libpq openssl py3-pillow RUN wget -O /usr/lib/python3.8/site-packages/diceware/wordlists/wordlist_cat_ascii.txt https://raw.githubusercontent.com/1ma/diceware-cat/master/cat-wordlist-ascii.txt diff --git a/admin/docker/requirements.pip3 b/admin/docker/requirements.pip3 index b7f1801..83f5d35 100644 --- a/admin/docker/requirements.pip3 +++ b/admin/docker/requirements.pip3 @@ -10,6 +10,7 @@ psycopg2==2.8.6 python-keycloak==0.26.1 minio==7.0.3 urllib3==1.26.6 +schema==0.7.5 # Unused yet #flask-oidc==1.4.0 \ No newline at end of file diff --git a/admin/src/admin/lib/dashboard.py b/admin/src/admin/lib/dashboard.py new file mode 100644 index 0000000..7504bc3 --- /dev/null +++ b/admin/src/admin/lib/dashboard.py @@ -0,0 +1,88 @@ +import logging as log +import traceback +from pprint import pprint + +import os, shutil + +from requests import get, post + +from schema import And, Optional, Schema, SchemaError, Use +import yaml + +from io import BytesIO +from PIL import Image + +from admin import app + + +class Dashboard: + def __init__( + self, + ): + self.custom_menu = os.path.join(app.root_path, "../custom/menu/custom.yaml") + + def _update_custom_menu(self, custom_menu_part): + with open(self.custom_menu) as yml: + menu = yaml.load(yml, Loader=yaml.FullLoader) + menu = {**menu, **custom_menu_part} + with open(self.custom_menu, "w") as yml: + yml.write(yaml.dump(menu, default_flow_style=False)) + return True + + def update_colours(self, colours): + schema_template = Schema( + { + "background": And(Use(str)), + "primary": And(Use(str)), + "secondary": And(Use(str)), + } + ) + + try: + schema_template.validate(colours) + except SchemaError: + return False + + self._update_custom_menu({"colours": colours}) + return self.apply_updates() + + def update_menu(self, menu): + for menu_item in menu.keys(): + for mustexist_key in ["href", "icon", "name", "shortname"]: + if mustexist_key not in menu[menu_item].keys(): + return False + self._update_custom_menu({"apps_external": menu}) + return self.apply_updates() + + def update_logo(self, logo): + img = Image.open(logo.stream) + img.save(os.path.join(app.root_path, "../custom/img/logo.png")) + img.save( + os.path.join( + app.root_path, + "../custom/system/keycloak/themes/liiibrelite/login/resources/img/logo.png", + ) + ) + return self.apply_updates() + + def update_background(self, background): + img = Image.open(background.stream) + img.save(os.path.join(app.root_path, "../custom/img/background.png")) + img.save( + os.path.join( + app.root_path, + "../custom/system/keycloak/themes/liiibrelite/login/resources/img/loginBG.png", + ) + ) + img.save( + os.path.join( + app.root_path, + "../custom/system/keycloak/themes/liiibrelite/login/resources/img/loginBG2.png", + ) + ) + return self.apply_updates() + + def apply_updates(self): + # Keycloak + # Api + return True diff --git a/admin/src/admin/static/dd.css b/admin/src/admin/static/dd.css index 8121edb..7c8e04e 100644 --- a/admin/src/admin/static/dd.css +++ b/admin/src/admin/static/dd.css @@ -100,10 +100,6 @@ table.dataTable tr.shown td.details-control > button > i:before { } /* Sidebar */ -.sidebar_logo { - width: 50px; - height: 50px; -} .sidebar-footer { background: #3b3e47; @@ -127,9 +123,17 @@ table.dataTable tr.shown td.details-control > button > i:before { .nav_title { background: #3b3e47; - margin-bottom: 15px; } .nav_menu { + padding: 10px; background: white; + height: 75px; + max-height: 75px; } + +.nav_menu_logo { + margin-top: 5px; + width: 100; + height: 45px; +} \ No newline at end of file diff --git a/admin/src/admin/static/js/dashboard.js b/admin/src/admin/static/js/dashboard.js new file mode 100644 index 0000000..812be50 --- /dev/null +++ b/admin/src/admin/static/js/dashboard.js @@ -0,0 +1,443 @@ + +$(document).ready(function() { + + $('.icon-dropdown').select2({ + width: "100%", + templateSelection: formatText, + templateResult: formatText + }); + + function formatText (icon) { + return $(' ' + icon.text + ''); + }; + + $('#colorpicker-background').colorpicker().on('changeColor', function (e) { + $('#colorpicker-background-input').val(e.color.toHex()); + $('.right_col').css('background-color', e.color.toHex()); + }); + + $('#colorpicker-background-input').on('keyDown', function (e) { + $('#colorpicker-background').val(e.color.toHex()); + }) + + $('#colorpicker-primary').colorpicker().on('changeColor', function (e) { + $('#colorpicker-primary-input').val(e.color.toHex()); + $('.left_col').css('background-color', e.color.toHex()); + $('body').css('background-color', e.color.toHex()); + }); + + $('#colorpicker-secondary').colorpicker().on('changeColor', function (e) { + $('#colorpicker-secondary-input').val(e.color.toHex()); + $('.left_col').css('color', e.color.toHex()); + }); + + init_logo_cropper() + init_background_cropper() + + $('#save-colors').click(function () { + console.log({ + 'background': $('#colorpicker-background-input').val(), + 'primary': $('#colorpicker-primary-input').val(), + 'secondary': $('#colorpicker-secondary-input').val() + }) + $.ajax({ + type: "PUT", + url:"/api/dashboard/colours", + data: JSON.stringify({ + 'background': $('#colorpicker-background-input').val(), + 'primary': $('#colorpicker-primary-input').val(), + 'secondary': $('#colorpicker-secondary-input').val() + }), + success: function(data) + { + $('#colorpicker-background').attr('data-container', data.colours.background); + $('#colorpicker-background-input').val(data.colours.background); + $('#colorpicker-primary').attr('data-container', data.colours.primary); + $('#colorpicker-primary-input').val(data.colours.primary); + $('#colorpicker-secondary').attr('data-container', data.colours.secondary); + $('#colorpicker-secondary-input').val(data.colours.secondary); + }, + error: function(data) + { + console.log('ERROR!') + console.log(data) + } + }); + }) + + $('#save-menu').click(function () { + ids = $('[id^="apps_external-"]') + var menu_options = {}; + ids.each(function( index ) { + // console.log(ids[index].id) + if(!(ids[index].id.split('-')[1] in menu_options)){ + menu_options[ids[index].id.split('-')[1]]={} + } + menu_options[ids[index].id.split('-')[1]][ids[index].id.split('-')[2]]=$('#'+ids[index].id).val() + }) + $.ajax({ + type: "PUT", + url:"/api/dashboard/menu", + data: JSON.stringify(menu_options), + success: function(data) + { + // $('#colorpicker-background').attr('data-container', data.colours.toHex()); + // $('#colorpicker-background-input').val(data.colours.toHex()); + // $('#colorpicker-primary').attr('data-container', data.colours.toHex()); + // $('#colorpicker-primary-input').val(data.colours.toHex()); + // $('#colorpicker-secondary').attr('data-container', data.colours.toHex()); + // $('#colorpicker-secondary-input').val(data.colours.toHex()); + }, + error: function(data) + { + console.log('ERROR!') + } + }); + }) + +}) + +/* CROPPER */ + + +function init_logo_cropper() { + + + if (typeof ($.fn.cropper) === 'undefined') { return; } + console.log('init_logo_cropper'); + + var $image = $('#image_logo'); + var $dataX = $('#dataX'); + var $dataY = $('#dataY'); + var $dataHeight = $('#dataHeight'); + var $dataWidth = $('#dataWidth'); + var $dataRotate = $('#dataRotate'); + var $dataScaleX = $('#dataScaleX'); + var $dataScaleY = $('#dataScaleY'); + var cropWidth = 80; + var cropHeight = 45; + var aspectRatio = cropWidth / cropHeight; + var options = { + aspectRatio: aspectRatio, + preview: '.img-preview-logo', + crop: function (e) { + $dataX.val(Math.round(e.x)); + $dataY.val(Math.round(e.y)); + $dataHeight.val(Math.round(e.height)); + $dataWidth.val(Math.round(e.width)); + $dataRotate.val(e.rotate); + $dataScaleX.val(e.scaleX); + $dataScaleY.val(e.scaleY); + } + }; + + + // Tooltip + $('[data-toggle="tooltip"]').tooltip(); + + + // Cropper + $image.on({ + 'build.cropper': function (e) { + console.log(e.type); + }, + 'built.cropper': function (e) { + console.log(e.type); + }, + 'cropstart.cropper': function (e) { + console.log(e.type, e.action); + }, + 'cropmove.cropper': function (e) { + console.log(e.type, e.action); + }, + 'cropend.cropper': function (e) { + console.log(e.type, e.action); + }, + 'crop.cropper': function (e) { + console.log(e.type, e.x, e.y, e.width, e.height, e.rotate, e.scaleX, e.scaleY); + }, + 'zoom.cropper': function (e) { + console.log(e.type, e.ratio); + } + }).cropper(options); + + $('#save-logo-crop').click(function () { + $image.data('cropper').getCroppedCanvas({ width: cropWidth, height: cropHeight }).toBlob(function (blob) { + var uri = URL.createObjectURL(blob); + var img = new Image(); + + img.src = uri; + $('.nav_menu_logo').attr('src', img.src) + + var formData = new FormData(); + formData.append('croppedImage', blob); + $.ajax('/api/dashboard/logo', { + method: "PUT", + data: formData, + processData: false, + contentType: false, + success: function () { + // Update logo image + $('.nav_menu_logo').attr('src', img.src) + console.log('Upload success'); + }, + error: function () { + console.log('Upload error'); + } + }); + }); + }) + + // Methods + $('.docs-buttons-logo').on('click', '[data-method]', function () { + var $this = $(this); + var data = $this.data(); + var $target; + var result; + + if ($this.prop('disabled') || $this.hasClass('disabled')) { + return; + } + + if ($image.data('cropper') && data.method) { + data = $.extend({}, data); // Clone a new one + + if (typeof data.target !== 'undefined') { + $target = $(data.target); + + if (typeof data.option === 'undefined') { + try { + data.option = JSON.parse($target.val()); + } catch (e) { + console.log(e.message); + } + } + } + + result = $image.cropper(data.method, data.option, data.secondOption); + + switch (data.method) { + case 'scaleX': + case 'scaleY': + $(this).data('option', -data.option); + break; + } + + if ($.isPlainObject(result) && $target) { + try { + $target.val(JSON.stringify(result)); + } catch (e) { + console.log(e.message); + } + } + + } + }); + + // Import image + var $inputImage = $('#inputImage'); + var URL = window.URL || window.webkitURL; + var blobURL; + + if (URL) { + $inputImage.change(function () { + var files = this.files; + var file; + + if (!$image.data('cropper')) { + return; + } + + if (files && files.length) { + file = files[0]; + + if (/^image\/\w+$/.test(file.type)) { + blobURL = URL.createObjectURL(file); + $image.one('built.cropper', function () { + + // Revoke when load complete + URL.revokeObjectURL(blobURL); + }).cropper('reset').cropper('replace', blobURL); + $inputImage.val(''); + } else { + window.alert('Please choose an image file.'); + } + } + }); + } else { + $inputImage.prop('disabled', true).parent().addClass('disabled'); + } + + +}; + +/* CROPPER --- end */ + +function init_background_cropper() { + + + if (typeof ($.fn.cropper) === 'undefined') { return; } + console.log('init_background_cropper'); + + var $image = $('#image_background'); + var $dataX = $('#dataX'); + var $dataY = $('#dataY'); + var $dataHeight = $('#dataHeight'); + var $dataWidth = $('#dataWidth'); + var $dataRotate = $('#dataRotate'); + var $dataScaleX = $('#dataScaleX'); + var $dataScaleY = $('#dataScaleY'); + var cropWidth = 1920; + var cropHeight = 1080; + var aspectRatio = cropWidth / cropHeight; + var options = { + aspectRatio: aspectRatio, + preview: '.img-preview-background', + crop: function (e) { + $dataX.val(Math.round(e.x)); + $dataY.val(Math.round(e.y)); + $dataHeight.val(Math.round(e.height)); + $dataWidth.val(Math.round(e.width)); + $dataRotate.val(e.rotate); + $dataScaleX.val(e.scaleX); + $dataScaleY.val(e.scaleY); + } + }; + + + // Tooltip + $('[data-toggle="tooltip"]').tooltip(); + + + // Cropper + $image.on({ + 'build.cropper': function (e) { + console.log(e.type); + }, + 'built.cropper': function (e) { + console.log(e.type); + }, + 'cropstart.cropper': function (e) { + console.log(e.type, e.action); + }, + 'cropmove.cropper': function (e) { + console.log(e.type, e.action); + }, + 'cropend.cropper': function (e) { + console.log(e.type, e.action); + }, + 'crop.cropper': function (e) { + console.log(e.type, e.x, e.y, e.width, e.height, e.rotate, e.scaleX, e.scaleY); + }, + 'zoom.cropper': function (e) { + console.log(e.type, e.ratio); + } + }).cropper(options); + + $('#save-background-crop').click(function () { + $image.data('cropper').getCroppedCanvas({ width: cropWidth, height: cropHeight }).toBlob(function (blob) { + var uri = URL.createObjectURL(blob); + var img = new Image(); + + img.src = uri; + + var formData = new FormData(); + formData.append('croppedImage', blob); + $.ajax('/api/dashboard/background', { + method: "PUT", + data: formData, + processData: false, + contentType: false, + success: function () { + // Update background image + console.log('Upload success'); + }, + error: function () { + console.log('Upload error'); + } + }); + }); + }) + + // Methods + $('.docs-buttons-background').on('click', '[data-method]', function () { + var $this = $(this); + var data = $this.data(); + var $target; + var result; + + if ($this.prop('disabled') || $this.hasClass('disabled')) { + return; + } + + if ($image.data('cropper') && data.method) { + data = $.extend({}, data); // Clone a new one + + if (typeof data.target !== 'undefined') { + $target = $(data.target); + + if (typeof data.option === 'undefined') { + try { + data.option = JSON.parse($target.val()); + } catch (e) { + console.log(e.message); + } + } + } + + result = $image.cropper(data.method, data.option, data.secondOption); + + switch (data.method) { + case 'scaleX': + case 'scaleY': + $(this).data('option', -data.option); + break; + } + + if ($.isPlainObject(result) && $target) { + try { + $target.val(JSON.stringify(result)); + } catch (e) { + console.log(e.message); + } + } + + } + }); + + // Import image + var $inputImage = $('#inputImage'); + var URL = window.URL || window.webkitURL; + var blobURL; + + if (URL) { + $inputImage.change(function () { + var files = this.files; + var file; + + if (!$image.data('cropper')) { + return; + } + + if (files && files.length) { + file = files[0]; + + if (/^image\/\w+$/.test(file.type)) { + blobURL = URL.createObjectURL(file); + $image.one('built.cropper', function () { + + // Revoke when load complete + URL.revokeObjectURL(blobURL); + }).cropper('reset').cropper('replace', blobURL); + $inputImage.val(''); + } else { + window.alert('Please choose an image file.'); + } + } + }); + } else { + $inputImage.prop('disabled', true).parent().addClass('disabled'); + } + + +}; \ No newline at end of file diff --git a/admin/src/admin/static/templates/base.html b/admin/src/admin/static/templates/base.html index 47f784a..76b5b84 100644 --- a/admin/src/admin/static/templates/base.html +++ b/admin/src/admin/static/templates/base.html @@ -94,7 +94,7 @@ - + diff --git a/admin/src/admin/static/templates/header.html b/admin/src/admin/static/templates/header.html index c40be62..cfe07f7 100644 --- a/admin/src/admin/static/templates/header.html +++ b/admin/src/admin/static/templates/header.html @@ -4,6 +4,7 @@ +