From 1f0905c6a7482bb64f0861112392db116c83895e Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 16 Oct 2025 14:02:56 +0200 Subject: [PATCH 01/43] chore: update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ea5aa42a3a..7d62c46bf6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Version 25.03.XX Enterprise Fixes: - [data-manager] Fixed segment data deletion +- [funnels] Fixed delete confirmation using correct button copy - [users] Fix add/remove user to profile group - [users] Remove link to profile group page after removing user from group From 7d74f5298dcebc3e69a4b00a62bdbd0a974aac7a Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Wed, 5 Nov 2025 10:59:05 +0000 Subject: [PATCH 02/43] chore: correctly structure table extension mixin and refactor code --- .../countly/vue/components/datatable.js | 512 ++++++++++-------- 1 file changed, 289 insertions(+), 223 deletions(-) diff --git a/frontend/express/public/javascripts/countly/vue/components/datatable.js b/frontend/express/public/javascripts/countly/vue/components/datatable.js index f2b65f01284..743bacdc06d 100644 --- a/frontend/express/public/javascripts/countly/vue/components/datatable.js +++ b/frontend/express/public/javascripts/countly/vue/components/datatable.js @@ -6,93 +6,148 @@ _mixins = countlyVue.mixins; var TableExtensionsMixin = { + // NOTE: since this is a mixin and props are component specific, we should not define props here + // mixins should be a complement to the component, not a dependency props: { - persistKey: { - type: String, - default: null + availableDynamicCols: { + default: () => [], + type: Array }, + dataSource: { - type: Object, - default: function() { - return null; - } + default: () => null, + type: Object }, - rows: { - type: Array, - default: function() { - return []; - } + + defaultSort: { + default: () => ({ prop: '_id', order: 'asc' }), + type: Object }, - availableDynamicCols: { - type: Array, - default: function() { - return []; - } + + displaySearch: { + default: true, + type: Boolean }, + paused: { - type: Boolean, - default: false - }, - displaySearch: { - type: Boolean, - default: true + default: false, + type: Boolean }, - defaultSort: { - type: Object, - default: function() { - return { prop: '_id', order: 'asc' }; - }, - required: false + + persistKey: { + default: null, + type: String }, + preventDefaultSort: { - type: Boolean, - default: false + default: false, + type: Boolean + }, + + rows: { + default: () => [], + type: Array } }, + + emits: [ + 'params-change', + 'search-query-changed' + ], + + data() { + const controlParams = this.getControlParams(); + + if (!Array.isArray(controlParams?.selectedDynamicCols)) { + controlParams.selectedDynamicCols = this.availableDynamicCols.reduce((acc, option) => { + if (option.default || option.required) { + acc.push(option.value); + } + + return acc; + }, []); + } + + return { + controlParams: controlParams, + + firstPage: 1 + }; + }, + computed: { - hasDynamicCols: function() { - return this.availableDynamicCols.length > 0; - }, - availableDynamicColsLookup: function() { - return this.availableDynamicCols.reduce(function(acc, col) { + availableDynamicColsLookup() { + return this.availableDynamicCols.reduce((acc, col) => { acc[col.value] = col; + return acc; }, {}); }, - publicDynamicCols: function() { - var self = this; - var cols = this.controlParams.selectedDynamicCols.map(function(val) { - return self.availableDynamicColsLookup[val]; - }).filter(function(val) { - return !!val; - }); - return cols; + + availablePages() { + const pages = []; + + // NOTE: We should use a better variable name than I, it is not clear what this variable refers to + for (let i = this.firstPage, I = Math.min(this.lastPage, 10000); i <= I; i++) { + pages.push(i); + } + + return pages; }, - localSearchedRows: function() { - var currentArray = this.rows.slice(); - if (this.displaySearch && this.controlParams.searchQuery) { - var queryLc = (this.controlParams.searchQuery + "").toLowerCase(); - currentArray = currentArray.filter(function(item) { - return Object.keys(item).some(function(fieldKey) { - if (item[fieldKey] === null || item[fieldKey] === undefined) { - return false; - } - return (item[fieldKey] + "").toLowerCase().indexOf(queryLc) > -1; - }); - }); + + dataView() { + return this.dataSource ? this.externalData : this.localDataView; + }, + + externalData() { + if (!this.dataSource) { + return []; } - return currentArray; + + const { dataAddress } = this.dataSource; + + return dataAddress.store.getters[dataAddress.path]; }, - localDataView: function() { - var currentArray = this.localSearchedRows; + + externalParams() { + if (!this.dataSource) { + return undefined; + } + + const { paramsAddress } = this.dataSource; + + return paramsAddress.store.getters[paramsAddress.path]; + }, + + externalStatus() { + if (!this.dataSource) { + return undefined; + } + + const { statusAddress } = this.dataSource; + + return statusAddress.store.getters[statusAddress.path]; + }, + + hasDynamicCols() { + return this.availableDynamicCols.length > 0; + }, + + lastPage() { + return this.totalPages; + }, + + localDataView() { + let currentArray = this.localSearchedRows; + const totalRows = currentArray.length; + if (this.controlParams.sort.length > 0) { - var sorting = this.controlParams.sort[0], - dir = sorting.type === "asc" ? 1 : -1; + const sorting = this.controlParams.sort[0]; + const sortingType = sorting.type === "asc" ? 1 : -1; - currentArray = currentArray.slice(); - currentArray.sort(function(a, b) { - var priA = a[sorting.field], - priB = b[sorting.field]; + currentArray.sort((a, b) => { + let priA = a[sorting.field]; + let priB = b[sorting.field]; if (typeof priA === 'object' && priA !== null && priA.sortBy) { priA = priA.sortBy; @@ -100,218 +155,166 @@ } if (priA < priB) { - return -dir; + return -sortingType; } + if (priA > priB) { - return dir; + return sortingType; } + return 0; }); } - var filteredTotal = currentArray.length; + + // NOTE: displayMode seams to be a prop from cly-datatable component, since this is a mixin, it should + // not reference variables from the component using it, beside being confusing, + // it can lead to hard to debug issues if (this.displayMode === 'list') { - this.controlParams.perPage = currentArray.length; + this.controlParams.perPage = totalRows; } - if (this.controlParams.perPage < currentArray.length) { - var startIndex = (this.controlParams.page - 1) * this.controlParams.perPage, - endIndex = startIndex + this.controlParams.perPage; + + if (this.controlParams.perPage < totalRows) { + const startIndex = (this.controlParams.page - 1) * this.controlParams.perPage; + const endIndex = startIndex + this.controlParams.perPage; + currentArray = currentArray.slice(startIndex, endIndex); } + return { + notFilteredTotalRows: this.rows.length, rows: currentArray, - totalRows: filteredTotal, - notFilteredTotalRows: this.rows.length + totalRows }; }, - dataView: function() { - if (this.dataSource) { - return this.externalData; - } - else { - return this.localDataView; + + localSearchedRows() { + let currentArray = this.rows; + + if (this.displaySearch && this.controlParams.searchQuery) { + const lowerCaseSearchQuery = (`${this.controlParams.searchQuery}`).toLowerCase(); + + currentArray = currentArray.filter(item => { + return Object.keys(item).some(fieldKey => { + if (!item[fieldKey]) { + return false; + } + + return (`${item[fieldKey]}`).toLowerCase().indexOf(lowerCaseSearchQuery) > -1; + }); + }); } + + return currentArray; }, - totalPages: function() { - return Math.ceil(this.dataView.totalRows / this.controlParams.perPage); - }, - lastPage: function() { - return this.totalPages; - }, - prevAvailable: function() { - return this.controlParams.page > this.firstPage; - }, - nextAvailable: function() { + + nextAvailable() { return this.controlParams.page < this.lastPage; }, - paginationInfo: function() { - var page = this.controlParams.page, - perPage = this.controlParams.perPage, - searchQuery = this.controlParams.searchQuery, - grandTotal = this.dataView.notFilteredTotalRows, - filteredTotal = this.dataView.totalRows, - startEntries = (page - 1) * perPage + 1, - endEntries = Math.min(startEntries + perPage - 1, filteredTotal), - info = this.i18n("common.table.no-data"); + + paginationInfo() { + const { page, perPage, searchQuery } = this.controlParams || {}; + const { notFilteredTotalRows: grandTotal, totalRows: filteredTotal } = this.dataView || {}; + + const startEntries = (page - 1) * perPage + 1; + const endEntries = Math.min(startEntries + perPage - 1, filteredTotal); + let info = this.i18n('common.table.no-data'); if (filteredTotal > 0) { - info = this.i18n("common.showing") - .replace("_START_", countlyCommon.formatNumber(startEntries)) - .replace("_END_", countlyCommon.formatNumber(endEntries)) - .replace("_TOTAL_", countlyCommon.formatNumber(filteredTotal)); + info = this.i18n('common.showing') + .replace('_START_', countlyCommon.formatNumber(startEntries)) + .replace('_END_', countlyCommon.formatNumber(endEntries)) + .replace('_TOTAL_', countlyCommon.formatNumber(filteredTotal)); } if (this.displaySearch && searchQuery) { - info += " " + this.i18n("common.filtered").replace("_MAX_", grandTotal); + info += `${this.i18n('common.filtered').replace('_MAX_', grandTotal)}`; } + return info; }, - externalData: function() { - if (!this.dataSource) { - return []; - } - var addr = this.dataSource.dataAddress; - return addr.store.getters[addr.path]; - }, - externalStatus: function() { - if (!this.dataSource) { - return undefined; - } - var addr = this.dataSource.statusAddress; - return addr.store.getters[addr.path]; + + prevAvailable() { + return this.controlParams.page > this.firstPage; }, - externalParams: function() { - if (!this.dataSource) { - return undefined; - } - var addr = this.dataSource.paramsAddress; - return addr.store.getters[addr.path]; + + publicDynamicCols() { + return this.controlParams.selectedDynamicCols.map(val => this.availableDynamicColsLookup[val]) + .filter(Boolean); }, - availablePages: function() { - var pages = []; - for (var i = this.firstPage, I = Math.min(this.lastPage, 10000); i <= I; i++) { - pages.push(i); - } - return pages; + + totalPages() { + return Math.ceil(this.dataView.totalRows / this.controlParams.perPage); } }, + watch: { controlParams: { deep: true, + handler: _.debounce(function() { this.triggerExternalSource(); this.setControlParams(); }, 500) }, - 'controlParams.page': function() { + + 'controlParams.page'() { this.checkPageBoundaries(); }, - 'controlParams.selectedDynamicCols': function() { + + 'controlParams.selectedDynamicCols'() { this.$refs.elTable.store.updateColumns(); // TODO: Hacky, check for memory leaks. }, - 'controlParams.searchQuery': function(newVal) { + + 'controlParams.searchQuery'(newVal) { this.$emit('search-query-changed', newVal); }, - lastPage: function() { + + lastPage() { this.checkPageBoundaries(); }, - paused: function(newVal) { + + paused(newVal) { if (newVal) { - this.dataSource.updateParams({ - ready: false - }); + this.dataSource.updateParams({ ready: false }); } else { this.triggerExternalSource(); } } }, - data: function() { - var controlParams = this.getControlParams(); - if (!controlParams.selectedDynamicCols || !Array.isArray(controlParams.selectedDynamicCols)) { - controlParams.selectedDynamicCols = this.availableDynamicCols.reduce(function(acc, option) { - if (option.default || option.required) { - acc.push(option.value); - } - return acc; - }, []); - } - - return { - controlParams: controlParams, - firstPage: 1 - }; + beforeDestroy() { + this.setControlParams(); }, - mounted: function() { + + mounted() { this.triggerExternalSource(); }, - beforeDestroy: function() { - this.setControlParams(); - }, + methods: { - checkPageBoundaries: function() { + checkPageBoundaries() { if (this.lastPage > 0 && this.controlParams.page > this.lastPage) { this.controlParams.page = this.lastPage; } + if (this.controlParams.page < 1) { this.controlParams.page = 1; } }, - goToFirstPage: function() { - this.controlParams.page = this.firstPage; - }, - goToLastPage: function() { - this.controlParams.page = this.lastPage; - }, - goToPrevPage: function() { - if (this.prevAvailable) { - this.controlParams.page--; - } - }, - goToNextPage: function() { - if (this.nextAvailable) { - this.controlParams.page++; - } - }, - onSortChange: function(elTableSorting) { - if (elTableSorting.order) { - this.updateControlParams({ - sort: [{ - field: elTableSorting.column.sortBy || elTableSorting.prop, - type: elTableSorting.order === "ascending" ? "asc" : "desc" - }] - }); - } - else { - this.updateControlParams({ - sort: [] - }); - } - }, - triggerExternalSource: function() { - if (!this.dataSource || this.paused) { - return; - } - if (this.dataSource.fetch) { - this.dataSource.fetch(this.controlParams); - } - this.$emit("params-change", this.controlParams); - }, - updateControlParams: function(newParams) { - _.extend(this.controlParams, newParams); - }, - getControlParams: function() { - var defaultState = { + + getControlParams() { + const defaultState = { page: 1, perPage: 10, searchQuery: '', - sort: [], - selectedDynamicCols: false + selectedDynamicCols: false, + sort: [] }; + if (this.defaultSort && this.preventDefaultSort === false) { defaultState.sort = [{ field: this.defaultSort.prop, - type: this.defaultSort.order === "ascending" ? "asc" : "desc" + type: this.defaultSort.order === 'ascending' ? 'asc' : 'desc' }]; } else { @@ -321,51 +324,114 @@ if (!this.persistKey) { return defaultState; } - var loadedState = localStorage.getItem(this.persistKey); + + const loadedState = localStorage.getItem(this.persistKey); + try { - if (countlyGlobal.member.columnOrder && countlyGlobal.member.columnOrder[this.persistKey].tableSortMap) { + if ( + countlyGlobal.member.columnOrder && + countlyGlobal.member.columnOrder[this.persistKey].tableSortMap + ) { defaultState.selectedDynamicCols = countlyGlobal.member.columnOrder[this.persistKey].tableSortMap; } + if (loadedState) { - var parsed = JSON.parse(loadedState); + const parsed = JSON.parse(loadedState); + defaultState.page = parsed.page; defaultState.perPage = parsed.perPage; defaultState.sort = parsed.sort; } + return defaultState; } catch (ex) { return defaultState; } }, - setControlParams: function() { + + goToFirstPage() { + this.controlParams.page = this.firstPage; + }, + + goToLastPage() { + this.controlParams.page = this.lastPage; + }, + + goToNextPage() { + if (this.nextAvailable) { + this.controlParams.page++; + } + }, + + goToPrevPage() { + if (this.prevAvailable) { + this.controlParams.page--; + } + }, + + onSortChange(elTableSorting) { + if (elTableSorting.order) { + this.updateControlParams({ + sort: [{ + field: elTableSorting.column.sortBy || elTableSorting.prop, + type: elTableSorting.order === "ascending" ? "asc" : "desc" + }] + }); + } + else { + this.updateControlParams({ sort: [] }); + } + }, + + setControlParams() { if (this.persistKey) { - var self = this; - var localControlParams = {}; + const localControlParams = {}; + localControlParams.page = this.controlParams.page; localControlParams.perPage = this.controlParams.perPage; localControlParams.sort = this.controlParams.sort; + localStorage.setItem(this.persistKey, JSON.stringify(localControlParams)); + $.ajax({ - type: "POST", - url: countlyGlobal.path + "/user/settings/column-order", data: { - "tableSortMap": this.controlParams.selectedDynamicCols, - "columnOrderKey": this.persistKey, - _csrf: countlyGlobal.csrf_token + columnOrderKey: this.persistKey, + _csrf: countlyGlobal.csrf_token, + tableSortMap: this.controlParams.selectedDynamicCols }, - success: function() { - //since countlyGlobal.member does not updates automatically till refresh + success: () => { + // NOTE: since countlyGlobal.member does not updates automatically till refresh if (!countlyGlobal.member.columnOrder) { countlyGlobal.member.columnOrder = {}; } - if (!countlyGlobal.member.columnOrder[self.persistKey]) { - countlyGlobal.member.columnOrder[self.persistKey] = {}; + + if (!countlyGlobal.member.columnOrder[this.persistKey]) { + countlyGlobal.member.columnOrder[this.persistKey] = {}; } - countlyGlobal.member.columnOrder[self.persistKey].tableSortMap = self.controlParams.selectedDynamicCols; - } + + countlyGlobal.member.columnOrder[this.persistKey].tableSortMap = this.controlParams.selectedDynamicCols; + }, + type: 'POST', + url: `${countlyGlobal.path}/user/settings/column-order` }); } + }, + + triggerExternalSource() { + if (!this.dataSource || this.paused) { + return; + } + + if (this.dataSource.fetch) { + this.dataSource.fetch(this.controlParams); + } + + this.$emit('params-change', this.controlParams); + }, + + updateControlParams(newParams) { + _.extend(this.controlParams, newParams); } } }; @@ -1058,4 +1124,4 @@ '\n' })); -}(window.countlyVue = window.countlyVue || {}, jQuery)); \ No newline at end of file +}(window.countlyVue = window.countlyVue || {}, jQuery)); From 8051e01a95bc486b670b5f6d16ea277982e4a40f Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Wed, 5 Nov 2025 12:10:13 +0000 Subject: [PATCH 03/43] feat: add prop to set default rows per page --- .../public/javascripts/countly/vue/components/datatable.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/express/public/javascripts/countly/vue/components/datatable.js b/frontend/express/public/javascripts/countly/vue/components/datatable.js index 743bacdc06d..8ab9da2bb0f 100644 --- a/frontend/express/public/javascripts/countly/vue/components/datatable.js +++ b/frontend/express/public/javascripts/countly/vue/components/datatable.js @@ -47,6 +47,11 @@ rows: { default: () => [], type: Array + }, + + perPage: { + default: 10, + type: Number } }, @@ -305,7 +310,7 @@ getControlParams() { const defaultState = { page: 1, - perPage: 10, + perPage: this.perPage, searchQuery: '', selectedDynamicCols: false, sort: [] From b1312aad4bd28919950677e82d97fdac1dd4398e Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Wed, 5 Nov 2025 12:15:37 +0000 Subject: [PATCH 04/43] chore: Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9482976da4..f5c001218b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Fixes: Enterprise Fixes: - [groups] Add logs for user updates - [surveys] Change question map log to debug log +- [users] Set correct users widget table rows amount according to selected setting Enterprise Fixes: - [data-manager] Fixed bug when merging events with ampersand symbol in the name From 370a051db5faa31eafe5d8ae8f00f970014bfbf4 Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Thu, 6 Nov 2025 12:00:15 +0700 Subject: [PATCH 05/43] Update changelog --- CHANGELOG.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f30ab32692..632ad91982b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,12 @@ Fixes: - [security] Fixed injection possibility on res.expose Enterprise Fixes: +- [data-manager] Fixed bug when merging events with ampersand symbol in the name - [groups] Add logs for user updates - [nps] Sort widgets by internal name and search by name or internal name - [surveys] Change question map log to debug log - [surveys] Sort widgets by internal name and search by name or internal name - -Enterprise Fixes: -- [data-manager] Fixed bug when merging events with ampersand symbol in the name +- [surveys] Handle multiple survey submission from same user based on survey visibility Dependencies: - Bump axios from 1.12.2 to 1.13.1 in /plugins/cognito From 31765fb12969c3f3264eee15d1c9e1df5e24a715 Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Pinto Date: Thu, 6 Nov 2025 12:11:03 +0000 Subject: [PATCH 06/43] Update CHANGELOG.md --- CHANGELOG.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f30ab32692..74f62e93f83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,15 @@ -## Version 25.03.xx +## Version 25.03.26 Fixes: - [push] Fixed timeout setting - [security] Fixed injection possibility on res.expose Enterprise Fixes: +- [data-manager] Fixed bug when merging events with ampersand symbol in the name - [groups] Add logs for user updates - [nps] Sort widgets by internal name and search by name or internal name - [surveys] Change question map log to debug log - [surveys] Sort widgets by internal name and search by name or internal name -Enterprise Fixes: -- [data-manager] Fixed bug when merging events with ampersand symbol in the name - Dependencies: - Bump axios from 1.12.2 to 1.13.1 in /plugins/cognito - Bump csvtojson from 1.1.12 to 2.0.14 From 932408e7849cc03a8a7995ed05461f9959bb5f9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Art=C5=ABrs=20Kadi=C4=B7is?= Date: Thu, 6 Nov 2025 14:31:11 +0200 Subject: [PATCH 07/43] Remove duplicate 'funnels' fix from CHANGELOG Removed duplicate entry for 'funnels' delete confirmation fix. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cfa254d0f5..42921b1be7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ Fixes: Enterprise Fixes: - [ab-testing] Add script for fixing variant cohort - [groups] Fix user permission update after updating user group permission +- [funnels] Fixed delete confirmation using correct button copy ## Version 25.03.24 Fixes: @@ -41,7 +42,6 @@ Enterprise Fixes: - [compliance-hub] Fixed query patterns - [data-manager] Fixed bug preventing transformation of events ending in a dot - [data-manager] Fixed segment data deletion -- [funnels] Fixed delete confirmation using correct button copy - [license] Stop sending metric after license expired - [users] Fix add/remove user to profile group - [users] Remove link to profile group page after removing user from group From 886fb2f7762f364432e3075b8167dede921c3f41 Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 6 Nov 2025 14:54:06 +0100 Subject: [PATCH 08/43] feat: add setting to disable public dashboards --- plugins/dashboards/api/api.js | 59 +++++++++++++++---- plugins/dashboards/frontend/app.js | 1 + .../public/javascripts/countly.views.js | 47 +++++++++------ .../public/localization/dashboards.properties | 2 + 4 files changed, 79 insertions(+), 30 deletions(-) diff --git a/plugins/dashboards/api/api.js b/plugins/dashboards/api/api.js index 4ff954cb067..5637f8b98e0 100644 --- a/plugins/dashboards/api/api.js +++ b/plugins/dashboards/api/api.js @@ -17,7 +17,8 @@ var pluginOb = {}, var ejs = require("ejs"); plugins.setConfigs("dashboards", { - sharing_status: true + sharing_status: true, + allow_public_dashboards: true }); (function() { @@ -455,17 +456,24 @@ plugins.setConfigs("dashboards", { groups = [groups]; } groups = groups.map(group_id => group_id + ""); + + var orConditions = [ + {owner_id: memberId}, + {shared_with_edit: memberId}, + {shared_with_view: memberId}, + {shared_email_view: memberEmail}, + {shared_email_edit: memberEmail}, + {shared_user_groups_edit: {$in: groups}}, + {shared_user_groups_view: {$in: groups}} + ]; + + var allowPublicDashboards = plugins.getConfig("dashboards").allow_public_dashboards; + if (allowPublicDashboards !== false) { + orConditions.push({share_with: "all-users"}); + } + filterCond = { - $or: [ - {owner_id: memberId}, - {share_with: "all-users"}, - {shared_with_edit: memberId}, - {shared_with_view: memberId}, - {shared_email_view: memberEmail}, - {shared_email_edit: memberEmail}, - {shared_user_groups_edit: {$in: groups}}, - {shared_user_groups_view: {$in: groups}} - ] + $or: orConditions }; } let projection = {}; @@ -686,6 +694,12 @@ plugins.setConfigs("dashboards", { sharedUserGroupView = []; } + var allowPublicDashboards = plugins.getConfig("dashboards").allow_public_dashboards; + if (shareWith === "all-users" && allowPublicDashboards === false) { + common.returnMessage(params, 400, 'Public dashboards are disabled'); + return true; + } + var sharing = checkSharingStatus(params.member, shareWith, sharedEmailEdit, sharedEmailView, sharedUserGroupEdit, sharedUserGroupView); if (!sharing) { @@ -978,6 +992,12 @@ plugins.setConfigs("dashboards", { sharedUserGroupView = []; } + var allowPublicDashboards = plugins.getConfig("dashboards").allow_public_dashboards; + if (shareWith === "all-users" && allowPublicDashboards === false) { + common.returnMessage(params, 400, 'Public dashboards are disabled'); + return true; + } + common.db.collection("dashboards").findOne({_id: common.db.ObjectID(dashboardId)}, function(err, dashboard) { if (err || !dashboard) { common.returnMessage(params, 400, "Dashboard with the given id doesn't exist"); @@ -1747,6 +1767,16 @@ plugins.setConfigs("dashboards", { } if (dashboard.share_with === "all-users") { + var allowPublicDashboards = plugins.getConfig("dashboards").allow_public_dashboards; + if (allowPublicDashboards === false) { + if (member._id + "" === dashboard.owner_id) { + return cb(null, true); + } + if (member.global_admin) { + return cb(null, true); + } + return cb(null, false); + } return cb(null, true); } @@ -1817,6 +1847,13 @@ plugins.setConfigs("dashboards", { return cb(null, false); } + if (dashboard.share_with === "all-users") { + var allowPublicDashboards = plugins.getConfig("dashboards").allow_public_dashboards; + if (allowPublicDashboards === false) { + return cb(null, false); + } + } + if ((Array.isArray(dashboard.shared_with_edit) && dashboard.shared_with_edit.indexOf(member._id + "") !== -1) || (Array.isArray(dashboard.shared_email_edit) && dashboard.shared_email_edit.indexOf(member.email) !== -1)) { return cb(null, true); diff --git a/plugins/dashboards/frontend/app.js b/plugins/dashboards/frontend/app.js index c623758c11c..cdb3316922d 100644 --- a/plugins/dashboards/frontend/app.js +++ b/plugins/dashboards/frontend/app.js @@ -10,6 +10,7 @@ var countlyFs = require('../../../api/utils/countlyFs.js'); plugin.renderDashboard = function(ob) { ob.data.countlyGlobal.sharing_status = plugins.getConfig("dashboards").sharing_status; + ob.data.countlyGlobal.allow_public_dashboards = plugins.getConfig("dashboards").allow_public_dashboards; }; plugin.staticPaths = function(app/*, countlyDb*/) { diff --git a/plugins/dashboards/frontend/public/javascripts/countly.views.js b/plugins/dashboards/frontend/public/javascripts/countly.views.js index 83883a0bef1..51b4b2c3cd2 100644 --- a/plugins/dashboards/frontend/public/javascripts/countly.views.js +++ b/plugins/dashboards/frontend/public/javascripts/countly.views.js @@ -484,25 +484,6 @@ saveButtonLabel: "", sharingAllowed: countlyGlobal.sharing_status || AUTHENTIC_GLOBAL_ADMIN, groupSharingAllowed: countlyGlobal.plugins.indexOf("groups") > -1 && AUTHENTIC_GLOBAL_ADMIN, - constants: { - sharingOptions: [ - { - value: "all-users", - name: this.i18nM("dashboards.share.all-users"), - description: this.i18nM("dashboards.share.all-users.description"), - }, - { - value: "selected-users", - name: this.i18nM("dashboards.share.selected-users"), - description: this.i18nM("dashboards.share.selected-users.description"), - }, - { - value: "none", - name: this.i18nM("dashboards.share.none"), - description: this.i18nM("dashboards.share.none.description"), - } - ] - }, sharedEmailEdit: [], sharedEmailView: [], sharedGroupEdit: [], @@ -511,6 +492,34 @@ }; }, computed: { + constants: function() { + var allSharingOptions = [ + { + value: "all-users", + name: this.i18nM("dashboards.share.all-users"), + description: this.i18nM("dashboards.share.all-users.description"), + }, + { + value: "selected-users", + name: this.i18nM("dashboards.share.selected-users"), + description: this.i18nM("dashboards.share.selected-users.description"), + }, + { + value: "none", + name: this.i18nM("dashboards.share.none"), + description: this.i18nM("dashboards.share.none.description"), + } + ]; + + var allowPublicDashboards = countlyGlobal.allow_public_dashboards !== false; + var sharingOptions = allowPublicDashboards ? allSharingOptions : allSharingOptions.filter(function(option) { + return option.value !== "all-users"; + }); + + return { + sharingOptions: sharingOptions + }; + }, canShare: function() { var canShare = this.sharingAllowed && (this.controls.initialEditedObject.is_owner || AUTHENTIC_GLOBAL_ADMIN); return canShare; diff --git a/plugins/dashboards/frontend/public/localization/dashboards.properties b/plugins/dashboards/frontend/public/localization/dashboards.properties index b5b456cc996..06b17334afc 100644 --- a/plugins/dashboards/frontend/public/localization/dashboards.properties +++ b/plugins/dashboards/frontend/public/localization/dashboards.properties @@ -45,6 +45,8 @@ dashboards.compared-to-prev-period = compared to prev.period dashboards.sharing_status = Allow Dashboard Sharing configs.help.dashboards-sharing_status = Enable dashboard sharing for users to share a dashboard with other users. If set to off, dashboards cannot be shared with others. +dashboards.allow_public_dashboards = Allow public dashboards +configs.help.dashboards-allow_public_dashboards = Allow sharing dashboards with all Countly dashboard users dashboards.access-denied = This dashboard is no longer shared with you or an error has occurred. Please ask your global administrator if you think this is due to an issue. dashbaords.access-denied-title = Access Denied dashboards.edit-access-denied = You don't have the edit permission for this dashboard. Please ask your global administrator if you think this is due to an issue. From 9a37e81e0809f9437e52e6c06f712e7191b76ad7 Mon Sep 17 00:00:00 2001 From: Cihad Tekin Date: Fri, 7 Nov 2025 15:54:51 +0300 Subject: [PATCH 09/43] [push] Fixed the options of the request being made during mime detection --- CHANGELOG.md | 4 ++++ plugins/push/api/proxy.js | 10 ++-------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b92fd74404..04c0664d309 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Version 25.03.XX +Fixes: +- [push] Fixed the options of the request being made during mime detection + ## Version 25.03.26 Fixes: - [push] Fixed timeout setting diff --git a/plugins/push/api/proxy.js b/plugins/push/api/proxy.js index 83a1bcadc79..d109469bdd6 100644 --- a/plugins/push/api/proxy.js +++ b/plugins/push/api/proxy.js @@ -147,10 +147,6 @@ function request(url, method, conf) { let opts = {}, proto; try { let u = new URL(url); - opts.host = u.hostname; - opts.port = u.port; - opts.path = u.pathname; - opts.protocol = u.protocol; proto = u.protocol.substring(0, u.protocol.length - 1); if (!protos[proto]) { return new Error('Invalid protocol in url ' + url); @@ -161,7 +157,7 @@ function request(url, method, conf) { } catch (e) { log.e('Failed to parse media URL', e); - opts = {method, url}; + opts = {method}; proto = url.substr(0, url.indexOf(':')); } @@ -175,9 +171,7 @@ function request(url, method, conf) { }); opts.agent = new Agent(); } - - opts.url = url; - return protos[proto].request(opts); + return protos[proto].request(url, opts); } From aeeaf611657c431ba6d92db4ea1acbfdd35894ab Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Wed, 5 Nov 2025 11:39:15 +0700 Subject: [PATCH 10/43] [alerts] Add created by user for concurrent user alerts --- plugins/alerts/frontend/public/javascripts/countly.models.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/alerts/frontend/public/javascripts/countly.models.js b/plugins/alerts/frontend/public/javascripts/countly.models.js index 5e8d3d112c4..e27d7b05a7a 100644 --- a/plugins/alerts/frontend/public/javascripts/countly.models.js +++ b/plugins/alerts/frontend/public/javascripts/countly.models.js @@ -569,7 +569,7 @@ compareValue: list[j].users, compareValue2: list[j].minutes, alertValues: list[j].email, - createdByUser: "-", + createdByUser: list[j].createdByUser || '-', _canUpdate: countlyAuth.validateUpdate( FEATURE_NAME, countlyGlobal.member, From 6720264c592953fbc3dcf92114fbef5799dde05f Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Mon, 10 Nov 2025 12:32:36 +0700 Subject: [PATCH 11/43] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d42de76574a..c85e5892cb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Fixes: Enterprise Fixes: - [surveys] Handle multiple survey submission from same user based on survey visibility +- [concurrent_users] Fix alert threshold comparison ## Version 25.03.26 Fixes: From a35d901d2192f8ff03b27353b418c4235db116ba Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Wed, 5 Nov 2025 10:05:52 +0700 Subject: [PATCH 12/43] [plugins] Fix search in feature management page --- plugins/plugins/frontend/public/templates/plugins.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/plugins/frontend/public/templates/plugins.html b/plugins/plugins/frontend/public/templates/plugins.html index ddabcc63c87..8553bd789d6 100644 --- a/plugins/plugins/frontend/public/templates/plugins.html +++ b/plugins/plugins/frontend/public/templates/plugins.html @@ -17,7 +17,7 @@ - + @@ -77,4 +77,4 @@ - \ No newline at end of file + From e36db974bec16c03241c8700f0a0f4f006dd1e86 Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Mon, 10 Nov 2025 15:47:31 +0000 Subject: [PATCH 13/43] fix: remove infinite loop warning from console --- .../platform/javascripts/countly.views.js | 467 ++++++++++-------- .../core/platform/templates/platform.html | 219 +++++--- 2 files changed, 413 insertions(+), 273 deletions(-) diff --git a/frontend/express/public/core/platform/javascripts/countly.views.js b/frontend/express/public/core/platform/javascripts/countly.views.js index 5813f5997f4..7074b68909a 100644 --- a/frontend/express/public/core/platform/javascripts/countly.views.js +++ b/frontend/express/public/core/platform/javascripts/countly.views.js @@ -1,281 +1,342 @@ /* global countlyVue,CV,countlyCommon*/ + +const PlatformTableView = countlyVue.views.create({ + computed: { + appPlatform() { + return this.$store.state.countlyDevicesAndTypes.appPlatform; + }, + + appPlatformRows() { + return this.appPlatform.chartData; + }, + + isLoading() { + return this.$store.state.countlyDevicesAndTypes.platformLoading; + } + }, + + methods: { + numberFormatter(row, col, value) { + return countlyCommon.formatNumber(value, 0); + } + }, + + template: CV.T('/core/platform/templates/platform_table.html') +}); + +const VersionsTableView = countlyVue.views.create({ + computed: { + appPlatform() { + return this.$store.state.countlyDevicesAndTypes.appPlatform; + }, + + appPlatformVersionRows() { + const platforms = this.appPlatform.versions; + + if (!this.selectedPlatform && platforms.length) { + this.selectedPlatform = platforms[0].label; + this.$store.dispatch('countlyDevicesAndTypes/onSetSelectedPlatform', this.selectedPlatform); + } + + for (let k = 0; k < platforms.length; k++) { + if (platforms[k].label === this.selectedPlatform) { + return platforms[k].data || []; + } + } + + return []; + }, + + choosePlatform() { + const display = []; + const platforms = this.appPlatform.versions; + + for (let k = 0; k < platforms.length; k++) { + display.push({ + name: platforms[k].label, + value: platforms[k].label + }); + } + + if (!this.selectedPlatform && display.length) { + this.selectedPlatform = display[0].value; + this.$store.dispatch('countlyDevicesAndTypes/onSetSelectedPlatform', this.selectedPlatform); + } + + return display; + }, + + isLoading() { + return this.$store.state.countlyDevicesAndTypes.isLoading; + }, + + selectedPlatform: { + get() { + return this.$store.state.countlyDevicesAndTypes.selectedPlatform; + }, + + set(value) { + this.$store.dispatch('countlyDevicesAndTypes/onSetSelectedPlatform', value); + }, + } + }, + + methods: { + numberFormatter(row, col, value) { + return countlyCommon.formatNumber(value, 0); + } + }, + + template: CV.T('/core/platform/templates/version_table.html') +}); + var AppPlatformView = countlyVue.views.create({ - template: CV.T("/core/platform/templates/platform.html"), - data: function() { + mixins: [countlyVue.container.dataMixin({ externalLinks: '/analytics/platforms/links' })], + + data() { return { - scrollCards: { - vuescroll: {}, - scrollPanel: { - initialScrollX: false, + breakdownScrollOps: { + bar: { + background: '#A7AEB8', + keepShow: true, + size: '6px', + specifyBorderRadius: '3px' }, + rail: { - gutterOfSide: "0px" + gutterOfEnds: '15px', + gutterOfSide: '1px', }, - bar: { - background: "#A7AEB8", - size: "6px", - specifyBorderRadius: "3px", - keepShow: false - } + + scrollPanel: { initialScrollX: false }, + + vuescroll: {} }, - breakdownScrollOps: { - vuescroll: {}, - scrollPanel: { - initialScrollX: false, + + chooseProperties: [ + { + name: CV.i18n('common.table.total-sessions'), + value: 't' }, - rail: { - gutterOfSide: "1px", - gutterOfEnds: "15px" + { + name: CV.i18n('common.table.total-users'), + value: 'u' }, - bar: { - background: "#A7AEB8", - size: "6px", - specifyBorderRadius: "3px", - keepShow: true + { + name: CV.i18n('common.table.new-users'), + value: 'n' } - }, + ], + description: CV.i18n('platforms.description'), - dynamicTab: "platform-table", - platformTabs: [ + + dynamicTab: 'platform-table', + + graphColors: [ + '#017AFF', + '#39C0C8', + '#F5C900', + '#6C47FF' + ], + + tabs: [ { + component: PlatformTableView, + + dataTestId: 'platforms-table', + + name: 'platform-table', + title: CV.i18n('platforms.title'), - name: "platform-table", - dataTestId: "platforms-table", - component: countlyVue.views.create({ - template: CV.T("/core/platform/templates/platform_table.html"), - computed: { - appPlatform: function() { - return this.$store.state.countlyDevicesAndTypes.appPlatform; - }, - appPlatformRows: function() { - return this.appPlatform.chartData; - }, - isLoading: function() { - return this.$store.state.countlyDevicesAndTypes.platformLoading; - } - }, - methods: { - numberFormatter: function(row, col, value) { - return countlyCommon.formatNumber(value, 0); - } - } - }) }, { - title: CV.i18n('platforms.versions'), - name: "version-table", - dataTestId: "versions-table", - component: countlyVue.views.create({ - template: CV.T("/core/platform/templates/version_table.html"), - computed: { - isLoading: function() { - return this.$store.state.countlyDevicesAndTypes.isLoading; - }, - appPlatform: function() { - return this.$store.state.countlyDevicesAndTypes.appPlatform; - }, - selectedPlatform: { - set: function(value) { - this.$store.dispatch('countlyDevicesAndTypes/onSetSelectedPlatform', value); - }, - get: function() { - return this.$store.state.countlyDevicesAndTypes.selectedPlatform; - } - }, - appPlatformVersionRows: function() { - var platforms = this.appPlatform.versions; - - if (!this.selectedPlatform && platforms.length) { - this.selectedPlatform = platforms[0].label; - this.$store.dispatch('countlyDevicesAndTypes/onSetSelectedPlatform', this.selectedPlatform); - } - for (var k = 0; k < platforms.length; k++) { - if (platforms[k].label === this.selectedPlatform) { - - return platforms[k].data || []; - } - } - - return []; - }, - choosePlatform: function() { - var platforms = this.appPlatform.versions; - var display = []; - for (var k = 0; k < platforms.length; k++) { - display.push({"value": platforms[k].label, "name": platforms[k].label}); - } - if (!this.selectedPlatform && display.length) { - this.selectedPlatform = display[0].value; - this.$store.dispatch('countlyDevicesAndTypes/onSetSelectedPlatform', this.selectedPlatform); - } - return display; - } - }, - methods: { - numberFormatter: function(row, col, value) { - return countlyCommon.formatNumber(value, 0); - } - } - }) + component: VersionsTableView, + + dataTestId: 'versions-table', + + name: 'version-table', + + title: CV.i18n('platforms.versions') } - ] - }; - }, - mounted: function() { - this.$store.dispatch('countlyDevicesAndTypes/fetchPlatform'); - }, - methods: { - refresh: function(force) { - if (force) { - this.$store.dispatch('countlyDevicesAndTypes/fetchPlatform', true); - } - else { - this.$store.dispatch('countlyDevicesAndTypes/fetchPlatform', false); - } - }, - dateChange: function() { - this.refresh(true); - }, - handleCardsScroll: function() { - if (this.$refs && this.$refs.bottomSlider) { - var pos1 = this.$refs.topSlider.getPosition(); - pos1 = pos1.scrollLeft; - this.$refs.bottomSlider.scrollTo({x: pos1}, 0); - } - }, - handleBottomScroll: function() { - if (this.$refs && this.$refs.topSlider) { - var pos1 = this.$refs.bottomSlider.getPosition(); - pos1 = pos1.scrollLeft; - this.$refs.topSlider.scrollTo({x: pos1}, 0); + ], + + scrollCards: { + bar: { + background: '#A7AEB8', + keepShow: false, + size: '6px', + specifyBorderRadius: '3px' + }, + + rail: { gutterOfSide: '0px' }, + + scrollPanel: { initialScrollX: false }, + + vuescroll: {}, } - } + }; }, + computed: { - selectedProperty: { - set: function(value) { - this.$store.dispatch('countlyDevicesAndTypes/onSetSelectedProperty', value); - }, - get: function() { - return this.$store.state.countlyDevicesAndTypes.selectedProperty; - }, - dropdownsDisabled: function() { - return ""; - } - }, - graphColors: function() { - return ["#017AFF", "#39C0C8", "#F5C900", "#6C47FF"]; - }, - appPlatform: function() { + appPlatform() { return this.$store.state.countlyDevicesAndTypes.appPlatform; }, - appResolution: function() { + + appPlatformRows() { + return this.appPlatform.chartData || []; + }, + + appResolution() { return this.$store.state.countlyDevicesAndTypes.appResolution; }, - chooseProperties: function() { - return [{"value": "t", "name": CV.i18n('common.table.total-sessions')}, {"value": "u", "name": CV.i18n('common.table.total-users')}, {"value": "n", "name": CV.i18n('common.table.new-users')}]; + + isLoading() { + return this.$store.state.countlyDevicesAndTypes.platformLoading; }, - platformItems: function() { - var display = []; - var property = this.$store.state.countlyDevicesAndTypes.selectedProperty; - var data = this.appPlatform.chartData || []; + platformItems() { + const data = JSON.parse(JSON.stringify(this.appPlatformRows)); + const display = []; + const property = this.$store.state.countlyDevicesAndTypes.selectedProperty; + + data.sort((a, b) => { + const totalDiff = b[property] - a[property]; - data.sort(function(a, b) { - let totalDiff = b[property] - a[property]; if (totalDiff === 0) { return a.os_.localeCompare(b.os_); } + return totalDiff; }); for (var k = 0; k < data.length; k++) { - var percent = Math.round((data[k][property] || 0) * 1000 / (this.appPlatform.totals[property] || 1)) / 10; + const percent = Math.round((data[k][property] || 0) * 1000 / (this.appPlatform.totals[property] || 1)) / 10; + display.push({ - "name": data[k].origos_, - "value": countlyCommon.getShortNumber(data[k][property] || 0), - "percent": percent, - "percentText": percent + "% " + CV.i18n('common.of-total'), - "info": "some description", - "color": this.graphColors[k % this.graphColors.length] + color: this.graphColors[k % this.graphColors.length], + info: 'some description', + name: data[k].origos_, + percent, + percentText: `${percent}% ${CV.i18n('common.of-total')}`, + value: countlyCommon.getShortNumber(data[k][property] || 0) }); } + return display; }, - topDropdown: function() { - if (this.externalLinks && Array.isArray(this.externalLinks) && this.externalLinks.length > 0) { - return this.externalLinks; - } - else { - return null; - } + platformVersions() { + const platforms = JSON.parse(JSON.stringify(this.appPlatform.versions || [])); + const property = this.$store.state.countlyDevicesAndTypes.selectedProperty; + const returnData = []; - }, - platformVersions: function() { - var property = this.$store.state.countlyDevicesAndTypes.selectedProperty; - var returnData = []; - var platforms = this.appPlatform.versions || []; for (var z = 0; z < platforms.length; z++) { - var display = []; var data = platforms[z].data; + for (var k = 0; k < data.length; k++) { var percent = Math.round((data[k][property] || 0) * 1000 / (platforms[z][property] || 1)) / 10; + display.push({ - "name": data[k].os_versions, - "description": countlyCommon.getShortNumber(data[k][property] || 0), - "percent": percent, - "bar": [{ - percentage: percent, - color: this.graphColors[z % this.graphColors.length] - } - ] + bar: [{ + color: this.graphColors[z % this.graphColors.length], + percentage: percent + }], + description: countlyCommon.getShortNumber(data[k][property] || 0), + name: data[k].os_versions, + percent }); } - returnData.push({"values": display, "label": platforms[z].label, itemCn: display.length}); + + returnData.push({ + itemCn: display.length, + label: platforms[z].label, + values: display + }); } const indexMap = {}; + this.platformItems.forEach((element, index) => { indexMap[element.name] = index; }); + returnData.sort((a, b) => { const nameA = a.label; const nameB = b.label; const indexA = indexMap[nameA]; const indexB = indexMap[nameB]; + return indexA - indexB; }); for (var i = 0; i < returnData.length; i++) { - returnData[i].values.sort(function(a, b) { - return parseFloat(b.percent) - parseFloat(a.percent); - }); + returnData[i].values.sort((a, b) => parseFloat(b.percent) - parseFloat(a.percent)); returnData[i].values = returnData[i].values.slice(0, 12); + // color adjustments after sorting platformVersions to match platformItems for (let index = 0; index < returnData[i].values.length; index++) { returnData[i].values[index].bar[0].color = this.platformItems[i].color; } } + return returnData; }, - appPlatformRows: function() { - return this.appPlatform.chartData; + + selectedProperty: { + dropdownsDisabled() { + return ''; + }, + + get() { + return this.$store.state.countlyDevicesAndTypes.selectedProperty; + }, + + set(value) { + this.$store.dispatch('countlyDevicesAndTypes/onSetSelectedProperty', value); + } }, - isLoading: function() { - return this.$store.state.countlyDevicesAndTypes.platformLoading; + + topDropdown() { + if (this.externalLinks && Array.isArray(this.externalLinks) && this.externalLinks.length > 0) { + return this.externalLinks; + } + + return null; + } + }, + + mounted() { + this.$store.dispatch('countlyDevicesAndTypes/fetchPlatform'); + }, + + methods: { + dateChange() { + this.refresh(true); + }, + + handleBottomScroll() { + if (this.$refs && this.$refs.topSlider) { + const x = this.$refs.bottomSlider.getPosition()?.scrollLeft; + + this.$refs.topSlider.scrollTo({ x }, 0); + } }, - tabs: function() { - return this.platformTabs; + + handleCardsScroll() { + if (this.$refs && this.$refs.bottomSlider) { + const x = this.$refs.topSlider.getPosition()?.scrollLeft; + + this.$refs.bottomSlider.scrollTo({ x }, 0); + } + }, + + refresh(force = false) { + this.$store.dispatch('countlyDevicesAndTypes/fetchPlatform', force); } }, - mixins: [ - countlyVue.container.dataMixin({ - 'externalLinks': '/analytics/platforms/links' - }) - ] + template: CV.T('/core/platform/templates/platform.html') }); @@ -287,4 +348,4 @@ countlyVue.container.registerTab("/analytics/technology", { title: CV.i18n('platforms.title'), dataTestId: "platforms", component: AppPlatformView -}); \ No newline at end of file +}); diff --git a/frontend/express/public/core/platform/templates/platform.html b/frontend/express/public/core/platform/templates/platform.html index f7c992f1cfd..e62da806bdb 100644 --- a/frontend/express/public/core/platform/templates/platform.html +++ b/frontend/express/public/core/platform/templates/platform.html @@ -1,72 +1,151 @@
- + + + + +
+

+ {{ i18n('platforms.platforms-for') }} +

+
+ + + +
+
+
- - - - -
-

{{i18n('platforms.platforms-for')}}

-
- - - -
-
-
- - - - {{item.name}} - - - -
-
{{i18n('common.table.no-data')}}
-
-
-
-
-
{{i18n('platforms.version-distribution')}}
-
-
{{i18n('common.table.no-data')}}
-
-
-
-
-
- - - - -
-
{{i18n('common.table.no-data')}}
-
-
-
-
- -
-
\ No newline at end of file + + + + {{ item.name }} + + + +
+
+ {{ i18n('common.table.no-data') }} +
+
+
+
+
+
+ {{ i18n('platforms.version-distribution') }} +
+
+
+ {{ i18n('common.table.no-data') }} +
+
+
+
+
+
+ + + +
+
+ {{ i18n('common.table.no-data') }} +
+
+
+
+
+ + +
From fdb3a9aa11c539226a808f7f349374f277cb8a0e Mon Sep 17 00:00:00 2001 From: Gabriel Oliveira Date: Tue, 11 Nov 2025 16:07:37 +0000 Subject: [PATCH 14/43] fix: target page console error on rating widgets table view load --- .../public/javascripts/countly.views.js | 161 ++++++++++-------- 1 file changed, 92 insertions(+), 69 deletions(-) diff --git a/plugins/star-rating/frontend/public/javascripts/countly.views.js b/plugins/star-rating/frontend/public/javascripts/countly.views.js index 3bfe2807424..be0addf9aca 100644 --- a/plugins/star-rating/frontend/public/javascripts/countly.views.js +++ b/plugins/star-rating/frontend/public/javascripts/countly.views.js @@ -393,19 +393,21 @@ }); var WidgetsTable = countlyVue.views.create({ + mixins: [ + countlyVue.mixins.auth(FEATURE_NAME), + countlyVue.mixins.commonFormatters + ], + props: { rows: { - type: Array, - default: [] + default: [], + type: Array } }, - mixins: [ - countlyVue.mixins.auth(FEATURE_NAME), - countlyVue.mixins.commonFormatters - ], + emits: ['widgets-updated'], - data: function() { + data() { return { cohortsEnabled: countlyGlobal.plugins.indexOf('cohorts') > -1, @@ -417,58 +419,67 @@ }; }, - emits: [ - 'widgets-updated' - ], - computed: { tableDynamicCols() { const columns = [ { - value: 'rating_score', - label: CV.i18n('feedback.rating-score'), default: true, - required: true + label: CV.i18n('feedback.rating-score'), + required: true, + value: 'rating_score' }, { - value: 'responses', - label: CV.i18n('feedback.responses'), default: true, - required: true + label: CV.i18n('feedback.responses'), + required: true, + value: 'responses' }, { - value: "target_pages", - label: CV.i18n("feedback.pages"), default: true, - required: true + label: CV.i18n("feedback.pages"), + required: true, + value: 'target_pages' } ]; if (this.cohortsEnabled) { columns.unshift({ - value: 'targeting', - label: CV.i18n('feedback.targeting'), default: true, - required: true + label: CV.i18n('feedback.targeting'), + required: true, + value: 'targeting' }); } return columns; }, - widgets: function() { - for (var i = 0; i < this.rows.length; i++) { - var ratingScore = 0; - if (this.rows[i].ratingsCount > 0) { - ratingScore = (this.rows[i].ratingsSum / this.rows[i].ratingsCount).toFixed(1); - } - this.rows[i].ratingScore = ratingScore; - this.rows[i].popup_header_text = replaceEscapes(this.rows[i].popup_header_text); - if (this.cohortsEnabled) { - this.rows[i] = this.parseTargeting(this.rows[i]); - } - this.rows[i].target_pages = this.rows[i].target_pages && this.rows[i].target_pages.length > 0 ? this.rows[i].target_pages.join(", ") : "-"; - } - return this.rows; + + widgets() { + return JSON.parse(JSON.stringify(this.rows)) + .map((widget) => { + const { + popup_header_text: popupHeaderText, + ratingsCount, + ratingsSum, + target_pages: targetPages, + targeting + } = widget || {}; + let ratingScore = 0; + + if (ratingsCount > 0) { + ratingScore = (ratingsSum / ratingsCount).toFixed(1); + } + + return { + ...widget, + popup_header_text: replaceEscapes(popupHeaderText), + ratingScore, + target_pages: Array.isArray(targetPages) && !!targetPages.length ? + targetPages.join(', ') : + '-', + ...this.cohortsEnabled && { targeting: this.parseTargeting(targeting) } + }; + }); } }, @@ -506,61 +517,73 @@ } }, - formatExportFunction: function() { - var tableData = this.widgets; - var table = []; - for (var i = 0; i < tableData.length; i++) { - var item = {}; - - item[CV.i18n('feedback.status').toUpperCase()] = tableData[i].status ? "Active" : "Inactive"; - item[CV.i18n('feedback.ratings-widget-name').toUpperCase()] = tableData[i].popup_header_text; - item[CV.i18n('feedback.widget-id').toUpperCase()] = tableData[i]._id; - item[CV.i18n('feedback.targeting').toUpperCase()] = this.parseTargetingForExport(tableData[i].targeting).trim(); - item[CV.i18n('feedback.rating-score').toUpperCase()] = tableData[i].ratingScore; - item[CV.i18n('feedback.responses').toUpperCase()] = tableData[i].ratingsCount; - item[CV.i18n('feedback.pages').toUpperCase()] = tableData[i].target_pages; + formatExportFunction() { + const table = []; + + for (let i = 0; i < this.widgets.length; i++) { + const item = {}; + const { + _id, + popup_header_text: popupHeaderText, + ratingScore, + ratingsCount, + status, + target_pages: targetPages, + targeting + } = this.widgets[i] || {}; + + item[CV.i18n('feedback.status').toUpperCase()] = status ? "Active" : "Inactive"; + item[CV.i18n('feedback.ratings-widget-name').toUpperCase()] = popupHeaderText; + item[CV.i18n('feedback.widget-id').toUpperCase()] = _id; + item[CV.i18n('feedback.targeting').toUpperCase()] = this.parseTargetingForExport(targeting).trim(); + item[CV.i18n('feedback.rating-score').toUpperCase()] = ratingScore; + item[CV.i18n('feedback.responses').toUpperCase()] = ratingsCount; + item[CV.i18n('feedback.pages').toUpperCase()] = targetPages; table.push(item); } - return table; + return table; }, - goWidgetDetail: function(row) { - window.location.hash = "#/" + countlyCommon.ACTIVE_APP_ID + "/feedback/ratings/widgets/" + row._id; + goWidgetDetail(row) { + window.location.hash = `#/${countlyCommon.ACTIVE_APP_ID}/feedback/ratings/widgets/${row._id}`; }, - parseTargeting: function(widget) { - if (widget.targeting) { + parseTargeting(unparsedTargeting) { + let targeting = JSON.parse(JSON.stringify(unparsedTargeting || null)); + + if (targeting) { + const { steps = null, user_segmentation: userSegmentation = null } = targeting || {}; try { - if (typeof widget.targeting.user_segmentation === "string") { - widget.targeting.user_segmentation = JSON.parse(widget.targeting.user_segmentation); + if (typeof userSegmentation === 'string') { + targeting.user_segmentation = JSON.parse(userSegmentation); } } catch (e) { - widget.targeting.user_segmentation = {}; + targeting.user_segmentation = {}; } try { - if (typeof widget.targeting.steps === "string") { - widget.targeting.steps = JSON.parse(widget.targeting.steps); + if (typeof steps === 'string') { + targeting.steps = JSON.parse(steps); } } catch (e) { - widget.targeting.steps = []; + targeting.steps = []; } - - widget.targeting.user_segmentation = widget.targeting.user_segmentation || {}; - widget.targeting.steps = widget.targeting.steps || []; } - return widget; + + return targeting; }, - parseTargetingForExport: function(widget) { - var targeting = countlyCohorts.getSegmentationDescription(widget); - var html = targeting.behavior; - var div = document.createElement('div'); + parseTargetingForExport(targeting) { + const segmentationDescription = countlyCohorts.getSegmentationDescription(targeting); + const html = segmentationDescription.behavior; + const div = document.createElement('div'); + div.innerHTML = html; + return div.textContent || div.innerText || ""; } }, From f5bd2b63eac76ad06e91f4f253244c9281ede4cc Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Tue, 11 Nov 2025 11:01:30 +0700 Subject: [PATCH 15/43] [views] Fix view name in table --- plugins/views/frontend/public/javascripts/countly.views.js | 5 +++-- plugins/views/frontend/public/templates/views.html | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/views/frontend/public/javascripts/countly.views.js b/plugins/views/frontend/public/javascripts/countly.views.js index fa38f427d65..28cd37364e9 100644 --- a/plugins/views/frontend/public/javascripts/countly.views.js +++ b/plugins/views/frontend/public/javascripts/countly.views.js @@ -6,7 +6,8 @@ var EditViewsView = countlyVue.views.create({ template: CV.T("/views/templates/manageViews.html"), mixins: [ - countlyVue.mixins.auth(FEATURE_NAME) + countlyVue.mixins.auth(FEATURE_NAME), + countlyVue.mixins.commonFormatters, ], data: function() { return { @@ -1248,4 +1249,4 @@ app.configurationsView.registerLabel("views", "views.title"); app.configurationsView.registerLabel("views.view_limit", "views.view-limit"); } -})(); \ No newline at end of file +})(); diff --git a/plugins/views/frontend/public/templates/views.html b/plugins/views/frontend/public/templates/views.html index 3b7aa82f65d..355208a3f03 100644 --- a/plugins/views/frontend/public/templates/views.html +++ b/plugins/views/frontend/public/templates/views.html @@ -77,7 +77,7 @@

{{i18n('views.for')}}

@@ -154,7 +154,7 @@

{{i18n('views.for')}}

From b7a8130f46820f532799e994f367168d0b40f3cd Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Tue, 11 Nov 2025 14:07:44 +0700 Subject: [PATCH 16/43] [core-vis] Move legend name escape to primary and secondary legend --- .../javascripts/countly/vue/components/vis.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/frontend/express/public/javascripts/countly/vue/components/vis.js b/frontend/express/public/javascripts/countly/vue/components/vis.js index 3481643564b..835990e5f0c 100644 --- a/frontend/express/public/javascripts/countly/vue/components/vis.js +++ b/frontend/express/public/javascripts/countly/vue/components/vis.js @@ -2146,6 +2146,9 @@ }); var SecondaryLegend = countlyBaseComponent.extend({ + mixins: [ + countlyVue.mixins.commonFormatters, + ], props: { data: { type: Array, @@ -2208,7 +2211,7 @@ {\'cly-vue-chart-legend__s-series--deselected\': item.status === \'off\'}]"\ @click="onClick(item, index)">\
\ -
{{item.label || item.name}}
\ +
{{unescapeHtml(item.label || item.name)}}
\
{{item.percentage}}%
\ \ \ @@ -2216,6 +2219,9 @@ }); var PrimaryLegend = countlyBaseComponent.extend({ + mixins: [ + countlyVue.mixins.commonFormatters, + ], props: { data: { type: Array, @@ -2240,7 +2246,7 @@ @click="onClick(item, index)">\
\
\ -
{{item.label || item.name}}
\ +
{{unescapeHtml(item.label || item.name)}}
\
\ \
\ @@ -2369,9 +2375,6 @@ } } } - data.forEach((item) => { - item.name = countlyCommon.unescapeHtml(item.name); - }); this.legendData = data; } } @@ -3652,4 +3655,4 @@ template: CV.T('/javascripts/countly/vue/templates/worldmap.html') })); -}(window.countlyVue = window.countlyVue || {})); \ No newline at end of file +}(window.countlyVue = window.countlyVue || {})); From 5a24948bda8645fc061dbb4ec621e47980c47c0f Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Wed, 12 Nov 2025 11:52:23 +0700 Subject: [PATCH 17/43] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a037e3dd38a..f0095223737 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## Version 25.03.XX Fixes: - [push] Fixed the options of the request being made during mime detection +- [views] Fix view name that is displayed in view table +- [core-vis] Fix chart legend click event Enterprise Fixes: - [concurrent_users] Fix alert threshold comparison From 83c31187fa63d689ad9d859f6f9cf15e3187fb2c Mon Sep 17 00:00:00 2001 From: Pavel Date: Wed, 12 Nov 2025 18:41:50 +0100 Subject: [PATCH 18/43] chore: update changelog --- CHANGELOG.md | 2 ++ countly-community | 1 + 2 files changed, 3 insertions(+) create mode 160000 countly-community diff --git a/CHANGELOG.md b/CHANGELOG.md index f0095223737..75afd0612e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Enterprise Fixes: - [concurrent_users] Fix alert threshold comparison - [surveys] Handle multiple survey submission from same user based on survey visibility - [users] Set correct users widget table rows amount according to selected setting +- [users] Display user property limits in user profiles when exceeded +- [dashboards] Add setting to disable public dashboards ## Version 25.03.26 Fixes: diff --git a/countly-community b/countly-community new file mode 160000 index 00000000000..f772bcd2c0e --- /dev/null +++ b/countly-community @@ -0,0 +1 @@ +Subproject commit f772bcd2c0e918dcf337ef3d978efbaa986bd882 From 0da221a72a09d00557a2eda9c6f5677d831b0221 Mon Sep 17 00:00:00 2001 From: Danu Widatama Date: Tue, 11 Nov 2025 10:06:41 +0700 Subject: [PATCH 19/43] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75afd0612e7..1b94c10dc17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Fixes: - [push] Fixed the options of the request being made during mime detection - [views] Fix view name that is displayed in view table - [core-vis] Fix chart legend click event +- [data-manager] Fix last modified data for event and segment Enterprise Fixes: - [concurrent_users] Fix alert threshold comparison From 1a810a2b1f5c6a14c955f3cd573b45947c0c1ce8 Mon Sep 17 00:00:00 2001 From: turtledreams Date: Thu, 13 Nov 2025 19:50:35 +0900 Subject: [PATCH 20/43] UI update --- .../public/javascripts/countly.views.js | 154 +++++++++++------- .../sdk/frontend/public/stylesheets/main.scss | 117 +++++++++++-- .../sdk/frontend/public/templates/config.html | 101 +++++++----- ui-tests/cypress/e2e/sdk/tool_01.cy.js | 2 +- ui-tests/cypress/e2e/sdk/tool_02.cy.js | 2 +- ui-tests/cypress/e2e/sdk/tool_03.cy.js | 2 +- ui-tests/cypress/e2e/sdk/tool_04.cy.js | 2 +- ui-tests/cypress/e2e/sdk/tool_05.cy.js | 2 +- ui-tests/cypress/e2e/sdk/tool_06.cy.js | 2 +- ui-tests/cypress/e2e/sdk/tool_07.cy.js | 2 +- ui-tests/cypress/e2e/sdk/tool_08.cy.js | 2 +- ui-tests/cypress/e2e/sdk/tool_09.cy.js | 2 +- ui-tests/cypress/lib/sdk/setup.js | 14 +- 13 files changed, 270 insertions(+), 134 deletions(-) diff --git a/plugins/sdk/frontend/public/javascripts/countly.views.js b/plugins/sdk/frontend/public/javascripts/countly.views.js index dd6e25b81e3..ea5dde851bf 100644 --- a/plugins/sdk/frontend/public/javascripts/countly.views.js +++ b/plugins/sdk/frontend/public/javascripts/countly.views.js @@ -15,6 +15,8 @@ var v2_web = "25.4.1"; var v2_flutter = "25.4.1"; var v2_react_native = "25.4.0"; + // dart sdk placeholder version to indicate experimental support + var v0_dart = "25.0.0"; // Supporting SDK Versions for the SC options var supportedSDKVersion = { tracking: { android: v0_android, ios: v0_ios, web: v1_web, flutter: v2_flutter, react_native: v2_react_native }, @@ -44,7 +46,17 @@ bom_at: { android: v2_android, ios: v2_ios, web: v2_web, flutter: v2_flutter, react_native: v2_react_native }, bom_rqp: { android: v2_android, ios: v2_ios, web: v2_web, flutter: v2_flutter, react_native: v2_react_native }, bom_ra: { android: v2_android, ios: v2_ios, web: v2_web, flutter: v2_flutter, react_native: v2_react_native }, - bom_d: { android: v2_android, ios: v2_ios, web: v2_web, flutter: v2_flutter, react_native: v2_react_native } + bom_d: { android: v2_android, ios: v2_ios, web: v2_web, flutter: v2_flutter, react_native: v2_react_native }, + upcl: { dart: v0_dart }, + filter_preset: { dart: v0_dart }, + eb: { dart: v0_dart }, + upb: { dart: v0_dart }, + sb: { dart: v0_dart }, + esb: { dart: v0_dart }, + ew: { dart: v0_dart }, + upw: { dart: v0_dart }, + sw: { dart: v0_dart }, + esw: { dart: v0_dart } }; var nonJSONExperimentalKeys = ['eb', 'upb', 'sb', 'ew', 'upw', 'sw']; @@ -115,6 +127,7 @@ this.$store.dispatch("countlySDK/initializeEnforcement"), this.$store.dispatch("countlySDK/fetchSDKStats") // fetch sdk version data for tooltips ]).then(function() { + log("SDK:init:complete", { params: self.$store.getters["countlySDK/sdk/all"], enforcement: self.$store.getters["countlySDK/sdk/enforcement"] }); self.$store.dispatch("countlySDK/sdk/setTableLoading", false); }); }, @@ -216,7 +229,7 @@ tracking: { type: "switch", name: "Allow Tracking", - description: "Enable or disable any tracking (gathering) of data in the SDK (default: enabled)", + description: "Enable or disable any tracking (gathering) of data in the SDK", default: true, enforced: false, value: null @@ -224,7 +237,7 @@ networking: { type: "switch", name: "Allow Networking", - description: "Enable or disable all networking calls from SDK except SDK behavior call. Does not effect tracking of data (default: enabled)", + description: "Enable or disable all networking calls from SDK except SDK behavior call. Does not effect tracking of data", default: true, enforced: false, value: null @@ -232,7 +245,7 @@ crt: { type: "switch", name: "Allow Crash Tracking", - description: "Enable or disable tracking of crashes (default: enabled)", + description: "Enable or disable tracking of crashes", default: true, enforced: false, value: null @@ -240,7 +253,7 @@ vt: { type: "switch", name: "Allow View Tracking", - description: "Enable or disable tracking of views (default: enabled)", + description: "Enable or disable tracking of views", default: true, enforced: false, value: null @@ -248,7 +261,7 @@ st: { type: "switch", name: "Allow Session Tracking", - description: "Enable or disable tracking of sessions (default: enabled)", + description: "Enable or disable tracking of sessions", default: true, enforced: false, value: null @@ -256,7 +269,7 @@ sui: { type: "number", name: "Session Update Interval", - description: "How often to send session update information to server in seconds (default: 60)", + description: "How often to send session update information to server in seconds", default: 60, enforced: false, value: null @@ -264,7 +277,7 @@ cet: { type: "switch", name: "Allow Custom Event Tracking", - description: "Enable or disable tracking of custom events (default: enabled)", + description: "Enable or disable tracking of custom events", default: true, enforced: false, value: null @@ -272,7 +285,7 @@ lt: { type: "switch", name: "Allow Location Tracking", - description: "Enable or disable tracking of location (default: enabled)", + description: "Enable or disable tracking of location", default: true, enforced: false, value: null @@ -280,7 +293,7 @@ ecz: { type: "switch", name: "Enable Content Zone", - description: "Enable or disable listening to Journey related contents (default: disabled)", + description: "Enable or disable listening to Journey related contents", default: false, enforced: false, value: null @@ -288,7 +301,7 @@ cr: { type: "switch", name: "Require Consent", - description: "Enable or disable requiring consent for tracking (default: disabled)", + description: "Enable or disable requiring consent for tracking", default: false, enforced: false, value: null @@ -296,7 +309,7 @@ rqs: { type: "number", name: "Request Queue Size", - description: "How many requests to store in queue, if SDK cannot connect to server (default: 1000)", + description: "How many requests to store in queue, if SDK cannot connect to server", default: 1000, enforced: false, value: null @@ -304,7 +317,7 @@ eqs: { type: "number", name: "Event Queue Size", - description: "How many events to store in queue before they would be batched and sent to server (default: 100)", + description: "How many events to store in queue before they would be batched and sent to server", default: 100, enforced: false, value: null @@ -312,7 +325,7 @@ czi: { type: "number", name: "Content Zone Interval", - description: "How often to check for new Journey content in seconds (default: 30, min: 15)", + description: "How often to check for new Journey content in seconds (min: 15)", default: 30, enforced: false, value: null @@ -320,7 +333,7 @@ dort: { type: "number", name: "Request Drop Age", - description: "Provide time in hours after which an old request should be dropped if they are not sent to server (default: 0 = disabled)", + description: "Provide time in hours after which an old request should be dropped if they are not sent to server (0 = disabled)", default: 0, enforced: false, value: null @@ -328,7 +341,7 @@ lkl: { type: "number", name: "Max Key Length", - description: "Maximum length of Event and segment keys (including name) (default: 128)", + description: "Maximum length of Event and segment keys (including name)", default: 128, enforced: false, value: null @@ -336,7 +349,7 @@ lvs: { type: "number", name: "Max Value Size", - description: "Maximum length of an Event's segment value (default: 256)", + description: "Maximum length of an Event's segment value", default: 256, enforced: false, value: null @@ -344,7 +357,7 @@ lsv: { type: "number", name: "Max Number of Segments", - description: "Maximum amount of segmentation key/value pairs per Event (default: 100)", + description: "Maximum amount of segmentation key/value pairs per Event", default: 100, enforced: false, value: null @@ -352,7 +365,7 @@ lbc: { type: "number", name: "Max Breadcrumb Count", - description: "Maximum breadcrumb count that can be provided by the developer (default: 100)", + description: "Maximum breadcrumb count that can be provided by the developer", default: 100, enforced: false, value: null @@ -360,7 +373,7 @@ ltlpt: { type: "number", name: "Max Trace Line Per Thread", - description: "Maximum stack trace lines that would be recorded per thread (default: 30)", + description: "Maximum stack trace lines that would be recorded per thread", default: 30, enforced: false, value: null @@ -368,7 +381,7 @@ ltl: { type: "number", name: "Max Trace Length Per Line", - description: "Maximum length of a stack trace line to be recorded (default: 200)", + description: "Maximum length of a stack trace line to be recorded", default: 200, enforced: false, value: null @@ -376,7 +389,7 @@ scui: { type: "number", name: "SDK Behavior Update Interval", - description: "How often to check for new behavior settings in hours (default: 4)", + description: "How often to check for new behavior settings in hours", default: 4, enforced: false, value: null @@ -384,7 +397,7 @@ rcz: { type: "switch", name: "Allow Refresh Content Zone", - description: "Enable or disable refreshing Journey content (default: enabled)", + description: "Enable or disable refreshing Journey content", default: true, enforced: false, value: null @@ -417,7 +430,7 @@ bom: { type: "switch", name: "Enable Backoff Mechanism", - description: "Enable or disable backoff mechanism for requests (default: enabled)", + description: "Enable or disable backoff mechanism for requests", default: true, enforced: false, value: null @@ -425,7 +438,7 @@ bom_at: { type: "number", name: "Backoff Timeout Limit", - description: "Maximum server delay acceptable before backoff mechanism can kick in (default: 10)", + description: "Maximum server delay acceptable before backoff mechanism can kick in", default: 10, enforced: false, value: null @@ -433,7 +446,7 @@ bom_rqp: { type: "number", name: "Backoff Requests Queue Percentage", - description: "Percentage of fullness that is acceptable for backoff mechanism to work (default: 50)", + description: "Percentage of fullness that is acceptable for backoff mechanism to work", default: 50, enforced: false, value: null @@ -441,7 +454,7 @@ bom_ra: { type: "number", name: "Backoff Requests Age", - description: "Maximum amount of request age(in hours) that is allowed in backoff (default: 24)", + description: "Maximum amount of request age(in hours) that is allowed in backoff", default: 24, enforced: false, value: null @@ -449,7 +462,7 @@ bom_d: { type: "number", name: "Backoff Delay", - description: "Delay in seconds that would be applied to requests in backoff (default: 60)", + description: "Delay in seconds that would be applied to requests in backoff", default: 60, enforced: false, value: null @@ -457,7 +470,7 @@ upcl: { type: "number", name: "User Property Cache", - description: "How many user property to store in cache before they would be batched and sent to server (default: 100)", + description: "How many user property to store in cache before they would be batched and sent to server", default: 100, enforced: false, value: null @@ -477,7 +490,7 @@ eb: { type: "text", name: "Event Blacklist", - description: "CSV* list of custom event keys to blacklist in SDK (default: empty)
* Use double quotes for values with commas", + description: "CSV* list of custom event keys to blacklist in SDK
* Use double quotes for values with commas", default: "", enforced: false, value: null, @@ -486,7 +499,7 @@ upb: { type: "text", name: "User Property Blacklist", - description: "CSV* list of user property keys to blacklist in SDK (default: empty)
* Use double quotes for values with commas", + description: "CSV* list of user property keys to blacklist in SDK
* Use double quotes for values with commas", default: "", enforced: false, value: null, @@ -495,7 +508,7 @@ sb: { type: "text", name: "Segmentation Blacklist", - description: "CSV* list of segmentation keys to blacklist in SDK (default: empty)
* Use double quotes for values with commas", + description: "CSV* list of segmentation keys to blacklist in SDK
* Use double quotes for values with commas", default: "", enforced: false, value: null, @@ -504,7 +517,7 @@ esb: { type: "text", name: "Event Segmentation Blacklist", - description: "Arrays of segmentation keys to blacklist for specific events (default: {})
Example: { \"event1\": [\"seg1\", \"seg2\"] }", + description: "Arrays of segmentation keys to blacklist for specific events
Example: { \"event1\": [\"seg1\", \"seg2\"] }", default: "{}", enforced: false, value: null, @@ -513,7 +526,7 @@ ew: { type: "text", name: "Event Whitelist", - description: "CSV* list of custom event keys to whitelist in SDK (default: empty)
* Use double quotes for values with commas", + description: "CSV* list of custom event keys to whitelist in SDK
* Use double quotes for values with commas", default: "", enforced: false, value: null, @@ -522,7 +535,7 @@ upw: { type: "text", name: "User Property Whitelist", - description: "CSV* list of user property keys to whitelist in SDK (default: empty)
* Use double quotes for values with commas", + description: "CSV* list of user property keys to whitelist in SDK
* Use double quotes for values with commas", default: "", enforced: false, value: null, @@ -531,7 +544,7 @@ sw: { type: "text", name: "Segmentation Whitelist", - description: "CSV* list of segmentation keys to whitelist in SDK (default: empty)
* Use double quotes for values with commas", + description: "CSV* list of segmentation keys to whitelist in SDK
* Use double quotes for values with commas", default: "", enforced: false, value: null, @@ -540,7 +553,7 @@ esw: { type: "text", name: "Event Segmentation Whitelist", - description: "Arrays of segmentation keys to whitelist for specific events (default: {})
Example: { \"event1\": [\"seg1\", \"seg2\"] }", + description: "Arrays of segmentation keys to whitelist for specific events
Example: { \"event1\": [\"seg1\", \"seg2\"] }", default: "{}", enforced: false, value: null, @@ -549,13 +562,14 @@ }, diff: [], validationErrors: {}, + isSaving: true, description: "Not all SDKs and SDK versions yet support this feature. Refer to respective SDK documentation for more information" }; }, mounted: function() { var self = this; this.$nextTick(function() { - self.checkSdkSupport(); + self.checkSdkSupport(0); self.isJSONInputValid('esb'); self.isJSONInputValid('esw'); }); @@ -618,7 +632,7 @@ }, onChange: function(key, value) { - log("onChange", key, value); + log("SDK:onChange", { key: key, value: value, diffLength: this.diff.length }); this.configs[key].value = value; if (key.startsWith("bom")) { this.getData.bom_preset.value = "Custom"; @@ -727,7 +741,7 @@ } }, downloadConfig: function() { - log("downloadConfig"); + log("SDK:downloadConfig:start"); var params = this.$store.getters["countlySDK/sdk/all"] || {}; // we change bom_rqp to decimal percentage for sdk if (typeof params.bom_rqp !== "undefined") { @@ -748,6 +762,7 @@ data.t = Date.now(); data.c = params; var configData = JSON.stringify(data, null, 2); + log("SDK:downloadConfig:payloadSize", configData.length); var blob = new Blob([configData], { type: 'application/json' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); @@ -759,6 +774,7 @@ URL.revokeObjectURL(url); }, onPresetChange(key, preset) { + log("SDK:onPresetChange", { key: key, preset: preset && preset.name }); this.getData[key].value = preset.name; if (this.diff.indexOf(key) === -1) { this.diff.push(key); @@ -799,11 +815,11 @@ if (key && !this.configs[key]) { key = null; // if key is not valid, enforce all } - var helper_msg = "You are about to enforce all current settings. This would override these settings in your SDK. Do you want to continue?"; - var helper_title = "Enforce all current settings?"; + var helper_msg = "You are about to apply all current settings. This will override these settings in your SDK. Do you want to continue?"; + var helper_title = "Apply all current settings?"; if (key) { - helper_msg = "You are about to enforce the current setting. This would override this setting in your SDK. Do you want to continue?"; - helper_title = "Enforce current setting?"; + helper_msg = "You are about to apply the current setting. This will override this setting in your SDK. Do you want to continue?"; + helper_title = "Apply current setting?"; } var self = this; log(`enforce:[${key}]`); @@ -831,7 +847,7 @@ } self.save(enforcement); }, - ["No, don't enforce", "Yes, enforce"], + ["Cancel", "Apply to SDKs"], { title: helper_title } ); }, @@ -839,8 +855,8 @@ if (key && !this.configs[key]) { return; } - var helper_msg = "You are about to revert the enforcement of the current setting. Your SDK would use default or developer set value if exist. Do you want to continue?"; - var helper_title = "Revert Enforced Setting?"; + var helper_msg = "SDKs will use their default or developer set behavior (if any). Are you sure you want to remove this setting?"; + var helper_title = "Remove from SDKs"; var self = this; CountlyHelpers.confirm(helper_msg, "red", function(result) { if (!result) { @@ -854,16 +870,19 @@ log(`reversing enforce key ${key}`); enforcement[key] = false; self.$store.dispatch("countlySDK/sdk/updateEnforcement", enforcement).then(function() { + log("SDK:reverseEnforce:dispatched", { key: key }); self.$store.dispatch("countlySDK/initializeEnforcement"); + }).catch(function(err) { + log("SDK:reverseEnforce:error", err); }); } }, - ["No, don't revert", "Yes, revert"], + ["Cancel", "Remove from SDKs"], { title: helper_title } ); }, resetSDKConfiguration: function() { - var helper_msg = "You are about to reset your SDK behavior to default state. This would override all these settings if set in your SDK. Do you want to continue?"; + var helper_msg = "You are about to reset all your SDK behavior settings values to their default state. Do you want to continue?"; var helper_title = "Reset Behavior?"; var self = this; @@ -875,6 +894,8 @@ var params = self.$store.getters["countlySDK/sdk/all"]; var data = params || {}; + log("SDK:reset:start", { currentParams: data }); + for (var key in self.configs) { var current = self.configs[key].value; var def = self.configs[key].default; @@ -914,10 +935,12 @@ CountlyHelpers.notify({ message: ex.message || 'Invalid experimental configuration', sticky: false, type: 'error' }); return; } + var changedKeys = self.diff.slice(); + log("SDK:reset:changedKeys", changedKeys); self.save(); } }, - ["No, don't reset", "Yes, reset"], + ["Cancel", "Reset to default"], { title: helper_title } ); }, @@ -929,8 +952,7 @@ return; } var params = this.$store.getters["countlySDK/sdk/all"]; - log(`save with enforcement: ${JSON.stringify(enforcement)}`); - log(`save: ${JSON.stringify(params)} and diff: ${JSON.stringify(this.diff)}`); + log("SDK:save:start", { enforcement: enforcement, params: params, diff: this.diff.slice() }); var data = params || {}; for (var i = 0; i < this.diff.length; i++) { var dkey = this.diff[i]; @@ -967,7 +989,7 @@ else { data[dkey] = val; } - log(`save: ${dkey} = ${JSON.stringify(data[dkey])}`); + log("SDK:save:field", { key: dkey, value: data[dkey] }); this.configs[dkey].enforced = true; } if (this.diff.indexOf('filter_preset') !== -1) { @@ -1006,10 +1028,16 @@ this.diff = []; var self = this; this.$store.dispatch("countlySDK/sdk/updateEnforcement", enforcement).then(() => { - this.$store.dispatch("countlySDK/initializeEnforcement"); + self.$store.dispatch("countlySDK/initializeEnforcement"); + log("SDK:updateEnforcement:dispatched", enforcement); + }).catch(function(err) { + log("SDK:updateEnforcement:error", err); }); this.$store.dispatch("countlySDK/sdk/update", data).then(function() { self.$store.dispatch("countlySDK/initialize"); + log("SDK:save:success", { data: data, enforcement: enforcement }); + }).catch(function(err) { + log("SDK:save:error", err); }); }, unpatch: function() { @@ -1098,12 +1126,12 @@ context.unsupportedList.push(text); } }, - checkSdkSupport: function() { + checkSdkSupport: function(retryCount) { log("checkSdkSupport"); for (var key in this.configs) { this.configs[key].experimental = false; this.configs[key].tooltipMessage = "No SDK data present. Please use the latest versions of Android, Web, iOS, Flutter or RN SDKs to use this option."; - if (key === 'upcl' || key === 'eb' || key === 'upb' || key === 'sb' || key === 'esb') { + if (key === 'upcl' || key === 'eb' || key === 'upb' || key === 'sb' || key === 'esb' || key === 'ew' || key === 'upw' || key === 'sw' || key === 'esw' || key === 'filter_preset') { this.configs[key].experimental = true; this.configs[key].tooltipMessage = "This is an experimental option. SDK support for this option may be limited or unavailable."; } @@ -1115,12 +1143,19 @@ !this.$store.state.countlySDK.stats.sdk || !this.$store.state.countlySDK.stats.sdk.versions || this.$store.state.countlySDK.stats.sdk.versions.length === 0) { + log("SDK data not yet available, retrying. countlySDK:[" + JSON.stringify(this.$store.state.countlySDK) + "]"); setTimeout(() => { - this.checkSdkSupport(); - }, 500); + retryCount = (retryCount || 0) + 1; + if (retryCount > 10) { + log("Max retries reached, giving up."); + return; + } + this.checkSdkSupport(retryCount); + }, 1000); return; } + log("SDK data available, processing:[" + JSON.stringify(this.$store.state.countlySDK.stats.sdk.versions) + "]"); const availableData = this.$store.state.countlySDK.stats.sdk.versions; const latestVersions = availableData.reduce((acc, sdk) => { if (!sdk.data || sdk.data.length === 0) { @@ -1147,7 +1182,8 @@ { label: 'js-rnb-android', configKey: 'react_native', name: 'React Native SDK' }, { label: 'js-rnbnp-android', configKey: 'react_native', name: 'React Native SDK' }, { label: 'js-rnb-ios', configKey: 'react_native', name: 'React Native SDK' }, - { label: 'js-rnbnp-ios', configKey: 'react_native', name: 'React Native SDK' } + { label: 'js-rnbnp-ios', configKey: 'react_native', name: 'React Native SDK' }, + { label: 'dart-native', configKey: 'dart', name: 'Dart SDK' } // TODO: this might change if naming changes ]; const uniqueLabels = new Set(); diff --git a/plugins/sdk/frontend/public/stylesheets/main.scss b/plugins/sdk/frontend/public/stylesheets/main.scss index 377d64e9f40..84ab53b36e9 100644 --- a/plugins/sdk/frontend/public/stylesheets/main.scss +++ b/plugins/sdk/frontend/public/stylesheets/main.scss @@ -72,21 +72,6 @@ } } -.cly-vue-tooltip-icon.ion.ion-help-circled.tooltip-success { - color: #6c3 !important; -} - -.cly-vue-tooltip-icon.ion.ion-help-circled.tooltip-warning { - color: #fc0 !important; -} - -.cly-vue-tooltip-icon.ion.ion-help-circled.tooltip-danger { - color: #f55 !important; -} - -.cly-vue-tooltip-icon.ion.ion-help-circled.tooltip-neutral { - color: #A7AEB8 !important; -} .preset-button { border-radius: 20px; // pill shape @@ -119,6 +104,37 @@ align-items: flex-start !important; } +@media (max-width: 1750px) { + .config-section .bu-column.bu-is-3.bu-is-flex.bu-is-justify-content-flex-start { + display: flex !important; + flex-direction: column !important; + gap: 12px !important; + align-items: stretch !important; + } + .sdk-preset-select-wrapper { margin-left: 0px !important; } + + .config-section .bu-column.bu-is-3.bu-is-flex.bu-is-justify-content-flex-start .sdk-preset-select-wrapper, + .config-section .bu-column.bu-is-3.bu-is-flex.bu-is-justify-content-flex-start .validation-wrapper, + .config-section .bu-column.bu-is-3.bu-is-flex.bu-is-justify-content-flex-start .sdk-number-wrapper, + .config-section .bu-column.bu-is-3.bu-is-flex.bu-is-justify-content-flex-start .el-select, + .config-section .bu-column.bu-is-3.bu-is-flex.bu-is-justify-content-flex-start .el-input, + .config-section .bu-column.bu-is-3.bu-is-flex.bu-is-justify-content-flex-start .el-input-number, + .config-section .bu-column.bu-is-3.bu-is-flex.bu-is-justify-content-flex-start .cly-colorpicker, + .config-section .bu-column.bu-is-3.bu-is-flex.bu-is-justify-content-flex-start .el-upload { + width: 100% !important; + min-width: 0 !important; + } + + .sdk-enforce-btn { + margin: 0 !important; + } + + .config-section .bu-column.bu-is-3.bu-is-flex.bu-is-justify-content-flex-start .sdk-enforce-btn { + min-width: 0 !important; + align-self: flex-start !important; + } +} + /* SDK config validation styles */ .config-invalid textarea, .config-invalid input, @@ -127,11 +143,14 @@ box-shadow: 0 0 0 1px rgba(231, 76, 60, 0.12) !important; } +.el-textarea { + width: 180px; +} + .validation-wrapper { display: flex; flex-direction: column; - width: 100%; - align-items: stretch; + // align-items: stretch; } .config-validation-box { @@ -168,3 +187,67 @@ } .config-validation-box .dot.green { background: var(--cly-success, #2ecc71); } .config-validation-box .dot.red { background: var(--cly-danger, #e74c3c); } + +.sdk-applied-indicator { + display: inline-flex; + align-items: center; + margin-left: 8px; + color: #10a14a; + font-size: 10px; + padding: 0 6px; + border-radius: 4px; + height: 20px; + line-height: 20px; + font-weight: 500; + white-space: nowrap; +} +.sdk-applied-indicator i { font-size: 12px; margin-right: 4px; } +.configuration-warning-container--applied { @extend .sdk-applied-indicator; background: #ebfaee; } + +.sdk-enforce-btn { margin-left: 8px; padding: 0 14px !important; display: inline-flex; align-items: center; justify-content: center; } +.sdk-enforce-btn i { margin-right: 6px; } +.sdk-enforce-btn.enforced { + background: #fff !important; + color: #34495e !important; + border-color: #dcdfe6 !important; + min-width: 180px; +} +.sdk-enforce-btn.enforced i { color: #c0bbbb; } + +.sdk-support-text { + display: flex; + align-items: flex-start; + gap: 6px; + font-size: 12px; + margin-top: 6px; + padding: 6px 8px; + border-radius: 6px; + border: 1px solid transparent; +} +.sdk-support-text i { font-size: 12px; margin-top: 2px; } +.sdk-support-text.tooltip-neutral { background: transparent; color: #0166D6; } +.sdk-support-text.tooltip-danger { background: transparent; color: #D23F00; } +.sdk-support-text.tooltip-success { background: transparent; color: #ffffff; } +.sdk-support-text.tooltip-warning { background: transparent; color: #E49700; } + +.sdk-default-flag { font-style: italic; color: #909399; } +.sdk-default-hint { margin-top: 4px; font-size: 11px; color: #909399; text-align: center; } +.sdk-enforce-fixed { min-width: 140px; text-align: center; } +.sdk-enforce-btn { align-self: flex-start; height: 32px !important; line-height: 30px !important; } + +.sdk-number-wrapper { display: flex; flex-direction: column; } +.sdk-number-wrapper .el-input-number { width: 180px; } +.el-select>.el-input { width: 180px; } +.sdk-switch-select { min-width: 180px; width: 180px; } +.sdk-preset-select-wrapper .el-select { width: 180px; } + +.sdk-switch-select { min-width: 140px; } + +.sdk-preset-select-wrapper { width: 100%; display: flex; justify-content: flex-start; margin-left: 188px; } + +.el-input-number { + .el-input-number__decrease, + .el-input-number__increase { + background: white; + } +} diff --git a/plugins/sdk/frontend/public/templates/config.html b/plugins/sdk/frontend/public/templates/config.html index ebe4dd5c3e2..1ee3ee57310 100644 --- a/plugins/sdk/frontend/public/templates/config.html +++ b/plugins/sdk/frontend/public/templates/config.html @@ -4,13 +4,13 @@ :tooltip="{description}">