From 5152931207a1e1e82870f08e3abd6150cb1ce842 Mon Sep 17 00:00:00 2001 From: Akhmadullin Roman Date: Mon, 30 Dec 2019 04:35:09 +0000 Subject: [PATCH] Fix bug with UserSettings in EE. polemarch/ee#34 --- doc/api_schema.yaml | 307 +++++++++++++------------ polemarch/api/v2/serializers.py | 6 + polemarch/static/css/polemarch-gui.css | 21 +- polemarch/static/js/pmDashboard.js | 91 ++++++-- polemarch/static/js/pmUsers.js | 96 +++++--- requirements-doc.txt | 2 +- requirements.txt | 2 +- 7 files changed, 301 insertions(+), 224 deletions(-) diff --git a/doc/api_schema.yaml b/doc/api_schema.yaml index cba13174..4df54f38 100755 --- a/doc/api_schema.yaml +++ b/doc/api_schema.yaml @@ -11,52 +11,52 @@ info: has_docs: true docs_url: /docs/ x-links: - Request: - - url: https://gitlab.com/vstconsulting/polemarch/issues/new?issuable_template%5D=Ask&issue%5Btitle%5D=Ask%20about%20version%201.6.0 - name: Question - - url: https://gitlab.com/vstconsulting/polemarch/issues/new?issuable_template%5D=Bug&issue%5Btitle%5D=Bug%20in%20version%201.6.0 - name: Bug report - - url: https://gitlab.com/vstconsulting/polemarch/issues/new?issuable_template%5D=Feature%20request&issue%5Btitle%5D= - name: Feature request Documentation: - url: http://polemarch.readthedocs.io/ name: Official documentation + url: http://polemarch.readthedocs.io/ Repository: - url: https://gitlab.com/vstconsulting/polemarch.git name: Official repository + url: https://gitlab.com/vstconsulting/polemarch.git + Request: + - name: Question + url: https://gitlab.com/vstconsulting/polemarch/issues/new?issuable_template%5D=Ask&issue%5Btitle%5D=Ask%20about%20version%201.6.2 + - name: Bug report + url: https://gitlab.com/vstconsulting/polemarch/issues/new?issuable_template%5D=Bug&issue%5Btitle%5D=Bug%20in%20version%201.6.2 + - name: Feature request + url: https://gitlab.com/vstconsulting/polemarch/issues/new?issuable_template%5D=Feature%20request&issue%5Btitle%5D= x-menu: - name: Projects - url: /project span_class: fa fa-fort-awesome + url: /project - name: Community - url: /community_template span_class: fa fa-cloud + url: /community_template - name: Inventories - url: /inventory span_class: fa fa-folder sublinks: - name: Groups - url: /group span_class: fa fa-tasks + url: /group - name: Hosts - url: /host span_class: fa fa-codepen + url: /host + url: /inventory - name: History - url: /history span_class: fa fa-calendar + url: /history - name: System span_class: fa fa-cog sublinks: - name: Users - url: /user span_class: fa fa-user + url: /user - name: Hooks - url: /hook span_class: fa fa-plug + url: /hook x-versions: - application: 1.6.0 - library: 1.6.0 - vstutils: 2.12.1 + application: 1.6.2 + library: 1.6.2 + vstutils: 2.15.1 django: 2.2.7 ansible: 2.9.2 version: v2 @@ -10953,14 +10953,14 @@ definitions: type: string format: dynamic additionalProperties: - field: key choices: {} + field: key types: - ansible_ssh_pass: password - ansible_ssh_private_key_file: secretfile ansible_become: boolean - ansible_port: integer ansible_become_pass: password + ansible_port: integer + ansible_ssh_pass: password + ansible_ssh_private_key_file: secretfile Host: type: object properties: @@ -11325,15 +11325,15 @@ definitions: format: dynamic default: NONE additionalProperties: - field: type choices: GIT: - NONE - KEY - PASSWORD + field: type types: - MANUAL: hidden GIT: string + MANUAL: hidden TAR: hidden auth_data: title: Repo auth data @@ -11341,22 +11341,22 @@ definitions: format: dynamic default: '' additionalProperties: - field: repo_auth choices: {} + field: repo_auth types: KEY: secretfile - PASSWORD: password NONE: hidden + PASSWORD: password branch: title: Branch for GIT(branch/tag/SHA) or TAR(subdir) type: string format: dynamic additionalProperties: - field: type choices: {} + field: type types: - MANUAL: hidden GIT: string + MANUAL: hidden TAR: string x-nullable: true OneProject: @@ -11597,55 +11597,10 @@ definitions: $ref: '#/definitions/Playbook' value_field: playbook view_field: name - verbose: - title: Verbose - description: verbose mode (-vvv for more, -vvvv to enable connection debugging) - type: integer - default: 0 - maximum: 4 - private_key: - title: Private key - description: use this file to authenticate the connection - type: string - format: secretfile - user: - title: User - description: connect as this user (default=None) - type: string - connection: - title: Connection - description: connection type to use (default=smart) - type: string - timeout: - title: Timeout - description: override the connection timeout in seconds (default=10) - type: integer - ssh_common_args: - title: Ssh common args - description: specify common arguments to pass to sftp/scp/ssh (e.g. ProxyCommand) - type: string - sftp_extra_args: - title: Sftp extra args - description: specify extra arguments to pass to sftp only (e.g. -f, -l) - type: string - scp_extra_args: - title: Scp extra args - description: specify extra arguments to pass to scp only (e.g. -l) - type: string - ssh_extra_args: - title: Ssh extra args - description: specify extra arguments to pass to ssh only (e.g. -R) + args: + title: Args + description: Playbook(s) type: string - force_handlers: - title: Force handlers - description: run handlers even if a task fails - type: boolean - default: false - flush_cache: - title: Flush cache - description: clear the fact cache for every host in inventory - type: boolean - default: false become: title: Become description: run operations with become (does not imply password prompting) @@ -11660,31 +11615,41 @@ definitions: title: Become user description: run operations as this user (default=root) type: string - tags: - title: Tags - description: only run plays and tasks tagged with these values - type: string - skip_tags: - title: Skip tags - description: only run plays and tasks whose tags do not match these values - type: string check: title: Check description: don't make any changes; instead, try to predict some of the changes that may occur type: boolean default: false - syntax_check: - title: Syntax check - description: perform a syntax check on the playbook, but do not execute it - type: boolean - default: false + connection: + title: Connection + description: connection type to use (default=smart) + type: string diff: title: Diff description: when changing (small) files and templates, show the differences in those files; works great with --check type: boolean default: false + extra_vars: + title: Extra vars + description: set additional variables as key=value or YAML/JSON, if filename + prepend with @ + type: string + flush_cache: + title: Flush cache + description: clear the fact cache for every host in inventory + type: boolean + default: false + force_handlers: + title: Force handlers + description: run handlers even if a task fails + type: boolean + default: false + forks: + title: Forks + description: specify number of parallel processes to use (default=5) + type: integer inventory: title: Inventory description: specify inventory host path or comma separated host list. --inventory-file @@ -11696,40 +11661,13 @@ definitions: $ref: '#/definitions/Inventory' value_field: id view_field: name - list_hosts: - title: List hosts - description: outputs a list of matching hosts; does not execute anything else - type: boolean - default: false limit: title: Limit description: further limit selected hosts to an additional pattern type: string - extra_vars: - title: Extra vars - description: set additional variables as key=value or YAML/JSON, if filename - prepend with @ - type: string - vault_id: - title: Vault id - description: the vault identity to use - type: string - vault_password_file: - title: Vault password file - description: vault password file - type: string - format: secretfile - forks: - title: Forks - description: specify number of parallel processes to use (default=5) - type: integer - module_path: - title: Module path - description: prepend colon-separated path(s) to module library (default=~/.ansible/plugins/modules:/usr/share/ansible/plugins/modules) - type: string - list_tasks: - title: List tasks - description: list all tasks that would be executed + list_hosts: + title: List hosts + description: outputs a list of matching hosts; does not execute anything else type: boolean default: false list_tags: @@ -11737,19 +11675,81 @@ definitions: description: list all available tags type: boolean default: false - step: - title: Step - description: 'one-step-at-a-time: confirm each task before running' + list_tasks: + title: List tasks + description: list all tasks that would be executed type: boolean default: false + module_path: + title: Module path + description: prepend colon-separated path(s) to module library (default=~/.ansible/plugins/modules:/usr/share/ansible/plugins/modules) + type: string + private_key: + title: Private key + description: use this file to authenticate the connection + type: string + format: secretfile + scp_extra_args: + title: Scp extra args + description: specify extra arguments to pass to scp only (e.g. -l) + type: string + sftp_extra_args: + title: Sftp extra args + description: specify extra arguments to pass to sftp only (e.g. -f, -l) + type: string + skip_tags: + title: Skip tags + description: only run plays and tasks whose tags do not match these values + type: string + ssh_common_args: + title: Ssh common args + description: specify common arguments to pass to sftp/scp/ssh (e.g. ProxyCommand) + type: string + ssh_extra_args: + title: Ssh extra args + description: specify extra arguments to pass to ssh only (e.g. -R) + type: string start_at_task: title: Start at task description: start the playbook at the task matching this name type: string - args: - title: Args - description: Playbook(s) + step: + title: Step + description: 'one-step-at-a-time: confirm each task before running' + type: boolean + default: false + syntax_check: + title: Syntax check + description: perform a syntax check on the playbook, but do not execute it + type: boolean + default: false + tags: + title: Tags + description: only run plays and tasks tagged with these values + type: string + timeout: + title: Timeout + description: override the connection timeout in seconds (default=10) + type: integer + user: + title: User + description: connect as this user (default=None) + type: string + vault_id: + title: Vault id + description: the vault identity to use + type: string + vault_password_file: + title: Vault password file + description: vault password file type: string + format: secretfile + verbose: + title: Verbose + description: verbose mode (-vvv for more, -vvvv to enable connection debugging) + type: integer + default: 0 + maximum: 4 ProjectHistory: required: - mode @@ -11879,22 +11879,22 @@ definitions: type: string format: dynamic additionalProperties: - field: kind choices: {} + field: kind types: - PLAYBOOK: fk_autocomplete MODULE: fk_autocomplete + PLAYBOOK: fk_autocomplete TEMPLATE: hidden inventory: title: Inventory type: string format: dynamic additionalProperties: - field: kind choices: {} + field: kind types: - PLAYBOOK: fk_autocomplete MODULE: fk_autocomplete + PLAYBOOK: fk_autocomplete TEMPLATE: hidden save_result: title: Save result @@ -11908,11 +11908,11 @@ definitions: type: string format: dynamic additionalProperties: - field: kind choices: {} + field: kind types: - PLAYBOOK: hidden MODULE: hidden + PLAYBOOK: hidden TEMPLATE: autocomplete enabled: title: Enabled @@ -11929,8 +11929,8 @@ definitions: type: string format: dynamic additionalProperties: - field: type choices: {} + field: type types: CRONTAB: crontab INTERVAL: integer @@ -11961,22 +11961,22 @@ definitions: type: string format: dynamic additionalProperties: - field: kind choices: {} + field: kind types: - PLAYBOOK: fk_autocomplete MODULE: fk_autocomplete + PLAYBOOK: fk_autocomplete TEMPLATE: hidden inventory: title: Inventory type: string format: dynamic additionalProperties: - field: kind choices: {} + field: kind types: - PLAYBOOK: fk_autocomplete MODULE: fk_autocomplete + PLAYBOOK: fk_autocomplete TEMPLATE: hidden save_result: title: Save result @@ -11990,11 +11990,11 @@ definitions: type: string format: dynamic additionalProperties: - field: kind choices: {} + field: kind types: - PLAYBOOK: hidden MODULE: hidden + PLAYBOOK: hidden TEMPLATE: autocomplete enabled: title: Enabled @@ -12011,8 +12011,8 @@ definitions: type: string format: dynamic additionalProperties: - field: type choices: {} + field: type types: CRONTAB: crontab INTERVAL: integer @@ -12176,20 +12176,20 @@ definitions: type: string format: dynamic additionalProperties: - field: key choices: + repo_sync_on_run: + - true + - false repo_type: - MANUAL - GIT - TAR - repo_sync_on_run: - - true - - false + field: key types: - repo_password: password + ci_template: fk repo_key: secretfile + repo_password: password repo_sync_on_run_timeout: uptime - ci_template: fk Team: required: - name @@ -12421,6 +12421,13 @@ definitions: - widgetSettings type: object properties: + lang: + title: Lang + type: string + enum: + - en + - ru + default: en autoupdateInterval: title: Autoupdateinterval type: integer @@ -12429,3 +12436,9 @@ definitions: $ref: '#/definitions/ChartLineSettings' widgetSettings: $ref: '#/definitions/WidgetSettings' + selectedSkin: + title: Selectedskin + type: string + minLength: 1 + skinsSettings: + $ref: '#/definitions/Data' diff --git a/polemarch/api/v2/serializers.py b/polemarch/api/v2/serializers.py index ed3767f5..6ecebfab 100644 --- a/polemarch/api/v2/serializers.py +++ b/polemarch/api/v2/serializers.py @@ -12,6 +12,7 @@ from vstutils.api.serializers import DataSerializer, EmptySerializer from vstutils.api.base import Response from ...main.utils import AnsibleArgumentsReference, AnsibleInventoryParser +from ...main.settings import LANGUAGES from ...main import models from ..signals import api_post_save, api_pre_save @@ -19,6 +20,8 @@ User = get_user_model() +LANG_CHOICES = [item[0] for item in LANGUAGES] + # NOTE: we can freely remove that because according to real behaviour all our # models always have queryset at this stage, so this code actually doing @@ -244,9 +247,12 @@ class WidgetSettingsSerializer(vst_serializers.JsonObjectSerializer): class UserSettingsSerializer(vst_serializers.JsonObjectSerializer): + lang = serializers.ChoiceField(choices=LANG_CHOICES, default=LANG_CHOICES[0]) autoupdateInterval = serializers.IntegerField(default=15000) chartLineSettings = ChartLineSettingsSerializer() widgetSettings = WidgetSettingsSerializer() + selectedSkin = serializers.CharField(required=False) + skinsSettings = vst_serializers.DataSerializer(required=False) class TeamSerializer(_WithPermissionsSerializer): diff --git a/polemarch/static/css/polemarch-gui.css b/polemarch/static/css/polemarch-gui.css index e02d3d71..cc41bdf6 100644 --- a/polemarch/static/css/polemarch-gui.css +++ b/polemarch/static/css/polemarch-gui.css @@ -180,39 +180,26 @@ body { --chart-axes-lines-color: #bababa; } - -.tr-status-offline .td-history-status, -.tr-status-offline .td-project_history-status, .field-status-offline { color: var(--history-status-offline); } -.tr-status-interrupted .td-history-status, -.tr-status-interrupted .td-project_history-status, .field-status-interrupted { color: var(--history-status-interrupted); } -.tr-status-run .td-history-status, -.tr-status-run .td-project_history-status, .field-status-run { color: var(--history-status-run); } -.tr-status-error .td-history-status, -.tr-status-error .td-project_history-status, .field-status-error { color: var(--history-status-error); } -.tr-status-delay .td-history-status, -.tr-status-delay .td-project_history-status, .field-status-delay { color: var(--history-status-delay); } -.tr-status-ok .td-history-status, -.tr-status-ok .td-project_history-status, .field-status-ok { color: var(--history-status-ok); } @@ -223,28 +210,22 @@ body { vertical-align: middle; } - -.tr-status-sync .td-project-status, .field-status-sync { color: var(--project-status-sync); } -.tr-status-wait_sync .td-project-status, .field-status-wait_sync { color: var(--project-status-wait-sync); } -.tr-status-new .td-project-status, .field-status-new { color: var(--project-status-new); } -.tr-status-error .td-project-status, .field-status-error { color: var(--project-status-error); } -.tr-status-ok .td-project-status, .field-status-ok { color: var(--project-status-ok); } @@ -365,7 +346,7 @@ body { float: left; } -@media (max-width: 452px) { +@media (max-width: 540px) { #period-list { width: 100%!important; margin-top: 10px; diff --git a/polemarch/static/js/pmDashboard.js b/polemarch/static/js/pmDashboard.js index 3451ba5d..2011b346 100644 --- a/polemarch/static/js/pmDashboard.js +++ b/polemarch/static/js/pmDashboard.js @@ -528,7 +528,14 @@ customRoutesComponentsTemplates.home = { /* globals customRoutesComponentsTempla this.setWidgetsData().then(data => { this.widgets_data = data; }); - } + }, + /** + * Saves widgets' data, when chart widget was collapsed/uncollapsed. + * @param value + */ + 'widgets.pmwChartWidget.collapse': function(value) { + this.saveWidgetSettingToApi('pmwChartWidget', 'collapse', value); + }, }, created() { this.fetchData(); @@ -598,18 +605,7 @@ customRoutesComponentsTemplates.home = { /* globals customRoutesComponentsTempla */ setWidgetsData() { return this.loadStats().then(response => { - let exclude_stats = ['jobs']; - let w_data = {}; - - for(let key in response.data) { - if (response.data.hasOwnProperty(key)) { - if (exclude_stats.includes(key)) { - w_data.pmwChartWidget = response.data[key]; - } - - w_data['pmw' + capitalizeString(key) + 'Counter'] = response.data[key]; - } - } + let w_data = this.statsResponseToWidgetData(response); this.$store.commit('setWidgets', { url: this.$route.path, @@ -631,10 +627,72 @@ customRoutesComponentsTemplates.home = { /* globals customRoutesComponentsTempla formBulkStats() { return { type: 'get', - item: 'stats', + data_type: ['stats'], filters: "last=" + this.widgets.pmwChartWidget.period.query_amount, }; }, + /** + * Method, that transforms API response with stats to widgets data. + * @param {object} response API response object + */ + statsResponseToWidgetData(response) { + let w_data = {}; + let exclude_stats = ['jobs']; + + for(let key in response.data) { + if (response.data.hasOwnProperty(key)) { + if (exclude_stats.includes(key)) { + w_data.pmwChartWidget = response.data[key]; + } + + w_data['pmw' + capitalizeString(key) + 'Counter'] = response.data[key]; + } + } + + return w_data; + }, + /** + * Method, that updates some property of widget and sends API request for saving updated User Settings. + * @param {string} widget Widget name + * @param {string} prop Widget's property name + * @param {any} value New value of widget's property name + */ + saveWidgetSettingToApi(widget, prop, value) { + let qs = app.application.$store.state.objects["user/" + my_user_id + "/settings"]; + + if(!qs) { + return; + } + + let instance = qs.cache; + + if(!instance) { + return; + } + + if(!instance.data) { + return; + } + + if(!instance.data.widgetSettings) { + instance.data.widgetSettings = {}; + } + + if(!instance.data.widgetSettings[widget]) { + instance.data.widgetSettings[widget] = {}; + } + + let widget_setting_backup = {...instance.data.widgetSettings[widget]}; + + instance.data.widgetSettings[widget][prop] = value; + + let view = app.views["/user/{" + path_pk_key + "}/settings/edit/"]; + instance.save(view.schema.query_type).then(instance => { + guiDashboard.updateSettings(instance.data); + }).catch(error => { /*jshint unused:false*/ + instance.data.widgetSettings[widget] = widget_setting_backup; + }); + }, }, }; @@ -642,10 +700,7 @@ tabSignal.connect('app.afterInit', (obj) => { let app = obj.app; let setting_view = app.views["/profile/settings/"]; let qs = setting_view.objects.clone(); - let f_obj = {}; - f_obj[path_pk_key] = my_user_id; - // qs.url = qs.url.format({pk:my_user_id}).replace(/^\/|\/$/g, ""); - qs.url = qs.url.format(f_obj).replace(/^\/|\/$/g, ""); + qs.url = qs.url.format({[path_pk_key]: my_user_id}).replace(/^\/|\/$/g, ""); qs.get().then(instance => { guiDashboard.updateSettings(instance.data); diff --git a/polemarch/static/js/pmUsers.js b/polemarch/static/js/pmUsers.js index f814ee27..4ed032e6 100644 --- a/polemarch/static/js/pmUsers.js +++ b/polemarch/static/js/pmUsers.js @@ -9,7 +9,9 @@ const user_settings_page_edit_mixin = { return; } - if(this.qs_url.replace(/^\/|\/$/g, "") == 'user/' + my_user_id + '/settings') { + let is_current_user_settings = this.qs_url.replace(/^\/|\/$/g, "") == 'user/' + my_user_id + '/settings'; + + if(is_current_user_settings) { data.selectedSkin = guiCustomizer.skin.name; data.skinsSettings = guiCustomizer.skins_custom_settings; } @@ -24,7 +26,9 @@ const user_settings_page_edit_mixin = { qs.cache = instance; this.setQuerySet(this.view, this.qs_url, qs); - guiDashboard.updateSettings(instance.data); + if(is_current_user_settings) { + guiDashboard.updateSettings(instance.data); + } guiPopUp.success(this.$t('User settings were successfully saved.')); @@ -99,46 +103,64 @@ function prepareUserSettingsViews(base_path) { } /** - * Signal, that adds 'lang' field to UserSettings model's fields. - * It supposed to be first during rendering. + * Function prepares fields of User Settings Model. + * @param {string} model name of model. */ -tabSignal.connect('openapi.loaded', openapi => { - openapi.definitions.UserSettings.properties = { - lang: {format: 'choices', title: 'language', description: 'application interface language'}, - ...openapi.definitions.UserSettings.properties, - }; -}); +function prepareUserSettingsModelFields(model) { + /** + * Signal, that edits options of UserSettings model's fields. + */ + tabSignal.connect("models[" + model + "].fields.beforeInit", (fields => { + if(fields.lang) { + fields.lang.title = 'language'; + fields.lang.description = 'application interface language'; + } + + if(fields.autoupdateInterval) { + fields.autoupdateInterval.format = 'time_interval'; + fields.autoupdateInterval.required = true; + fields.autoupdateInterval.title = 'Auto update interval'; + fields.autoupdateInterval.description = 'application automatically updates pages data' + + ' with following interval (time in seconds)'; + } + + [ + {name: 'chartLineSettings', title: "Dashboard chart lines settings", }, + {name: 'widgetSettings', title: "Dashboard widgets settings"}, + ].forEach((item) => { + if(fields[item.name]) { + fields[item.name].format = 'inner_api_object'; + fields[item.name].title = item.title; + } + }); + + if(fields.selectedSkin) { + fields.selectedSkin = { + title: 'Selected skin', + format: 'hidden', + }; + } + + if(fields.skinsSettings) { + fields.skinsSettings = { + title: 'Skin settings', + format: 'hidden', + }; + } + })); +} /** - * Signal, that edits options of UserSettings model's fields. + * Variable, that stores name of user Settings model. */ -tabSignal.connect("models[UserSettings].fields.beforeInit", (fields => { - [ - {name: 'chartLineSettings', title: "Dashboard chart lines settings", }, - {name: 'widgetSettings', title: "Dashboard widgets settings"}, - ].forEach((item) => { - fields[item.name].format = 'inner_api_object'; - fields[item.name].title = item.title; - }); +let user_settings_model_name = 'UserSettings'; + +/** + * Prepares fields of user settings model. + */ +prepareUserSettingsModelFields(user_settings_model_name); - fields.autoupdateInterval.format = 'time_interval'; - fields.autoupdateInterval.required = true; - fields.autoupdateInterval.title = 'Auto update interval'; - fields.autoupdateInterval.description = 'application automatically updates pages data' + - ' with following interval (time in seconds)'; - - fields.selectedSkin = { - title: 'Selected skin', - format: 'hidden', - }; - fields.skinsSettings = { - title: 'Skin settings', - format: 'hidden', - }; - - fields.lang.enum = app.languages.map(lang => lang.code); -})); /** * Emits signals for UserSettings views. */ -prepareUserSettingsViews('/user/{' + path_pk_key + '}/settings/'); \ No newline at end of file +prepareUserSettingsViews('/user/{' + path_pk_key + '}/settings/'); diff --git a/requirements-doc.txt b/requirements-doc.txt index 586accd0..41bac5d4 100644 --- a/requirements-doc.txt +++ b/requirements-doc.txt @@ -1,2 +1,2 @@ # Docs -vstutils[doc]~=2.14.1 +vstutils[doc]~=2.15.1 diff --git a/requirements.txt b/requirements.txt index 5ce3dfc2..51694bd4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # Main -vstutils[rpc,ldap,doc,prod]~=2.14.1 +vstutils[rpc,ldap,doc,prod]~=2.15.1 docutils==0.15.2 markdown2==2.3.8