diff --git a/analytics-api/src/main/java/io/meeds/analytics/api/service/AnalyticsService.java b/analytics-api/src/main/java/io/meeds/analytics/api/service/AnalyticsService.java index 9cff25b28..a3b2f4ed2 100644 --- a/analytics-api/src/main/java/io/meeds/analytics/api/service/AnalyticsService.java +++ b/analytics-api/src/main/java/io/meeds/analytics/api/service/AnalyticsService.java @@ -35,6 +35,7 @@ import io.meeds.analytics.model.filter.AnalyticsPeriod; import io.meeds.analytics.model.filter.AnalyticsPeriodType; import io.meeds.analytics.model.filter.AnalyticsTableFilter; +import io.meeds.analytics.model.filter.aggregation.AnalyticsAggregationType; public interface AnalyticsService { @@ -148,4 +149,49 @@ PercentageChartValue computePercentageChartData(AnalyticsFilter filter, */ void addUIWatcher(StatisticWatcher uiWatcher); + /** + * @param operations (Optional) {@link List} of collected operations to + * consider + * @param fieldName (Optional) Field Name to filter + * @param fieldValues (Optional) Field values to filter + * @param sortBy Sort field name + * @param sortDirection Sort direction: asc or desc + * @param limit limit of results to retrieve + * @return {@link List} of {@link StatisticData} representing the collected + * data + */ + List getSamples(List operations, + String fieldName, + List fieldValues, + String sortBy, + String sortDirection, + int limit); + + /** + * @param operations (Optional) {@link List} of collected operations to + * consider + * @param fieldName (Optional) Field Name to filter + * @param fieldValues (Optional) Field values to filter + * @param xAggregationField x axis field + * @param xAggregationType x axis aggregation type: MAX, MIN, SUM, COUNT, + * CARDINALITY (COUNT Distinct), TERMS, DATA or HISTOGRAM + * @param xAggregationSortDirection sort direction: asc or desc + * @param yAggregationField y axis field + * @param yAggregationType y axis aggregation type: MAX, MIN, SUM, COUNT, + * CARDINALITY (COUNT Distinct), TERMS, DATA or HISTOGRAM + * @param yAggregationSortDirection sort direction: asc or desc + * @param limit limit of x Axis results to retrieve + * @return ChartDataList + */ + ChartDataList getChart(List operations, // NOSONAR + String fieldName, + List fieldValues, + String xAggregationField, + AnalyticsAggregationType xAggregationType, + String xAggregationSortDirection, + String yAggregationField, + AnalyticsAggregationType yAggregationType, + String yAggregationSortDirection, + int limit); + } diff --git a/analytics-api/src/main/java/io/meeds/analytics/utils/AnalyticsUtils.java b/analytics-api/src/main/java/io/meeds/analytics/utils/AnalyticsUtils.java index 01e9b4a0b..64636d75a 100644 --- a/analytics-api/src/main/java/io/meeds/analytics/utils/AnalyticsUtils.java +++ b/analytics-api/src/main/java/io/meeds/analytics/utils/AnalyticsUtils.java @@ -422,6 +422,7 @@ public static void addSpaceStatistics(StatisticData statisticData, Space space) return; } statisticData.setSpaceId(Long.parseLong(space.getId())); + statisticData.addParameter("spaceTemplateId", space.getTemplateId()); statisticData.addParameter("spaceVisibility", space.getVisibility()); statisticData.addParameter("spaceRegistration", space.getRegistration()); statisticData.addParameter("spaceCreatedTime", space.getCreatedTime()); diff --git a/analytics-listeners/src/main/java/io/meeds/analytics/listener/social/AnalyticsActivityListener.java b/analytics-listeners/src/main/java/io/meeds/analytics/listener/social/AnalyticsActivityListener.java index 264f76187..3b3f812fe 100644 --- a/analytics-listeners/src/main/java/io/meeds/analytics/listener/social/AnalyticsActivityListener.java +++ b/analytics-listeners/src/main/java/io/meeds/analytics/listener/social/AnalyticsActivityListener.java @@ -156,6 +156,17 @@ public void likeComment(ActivityLifeCycleEvent event) { } } + @Override + public void pinActivity(ActivityLifeCycleEvent event) { + try { + StatisticData statisticData = addActivityStatisticEvent(event, "pinActivity"); + statisticData.setUserId(Long.parseLong(event.getUserId())); + addStatisticData(statisticData); + } catch (Exception e) { + handleErrorProcessingOperation(event, e); + } + } + private void handleErrorProcessingOperation(ActivityLifeCycleEvent event, Exception exception) { LOG.warn("Error adding Statistic data for activity {} with event {}", event.getActivityId(), event.getType(), exception); } diff --git a/analytics-services/src/main/java/io/meeds/analytics/elasticsearch/service/ElasticsearchAnalyticsService.java b/analytics-services/src/main/java/io/meeds/analytics/elasticsearch/service/ElasticsearchAnalyticsService.java index 6150cadd2..bb860d9bd 100644 --- a/analytics-services/src/main/java/io/meeds/analytics/elasticsearch/service/ElasticsearchAnalyticsService.java +++ b/analytics-services/src/main/java/io/meeds/analytics/elasticsearch/service/ElasticsearchAnalyticsService.java @@ -45,6 +45,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.json.JSONArray; import org.json.JSONException; @@ -87,6 +88,7 @@ import io.meeds.analytics.model.filter.aggregation.AnalyticsAggregationType; import io.meeds.analytics.model.filter.aggregation.AnalyticsPercentageLimit; import io.meeds.analytics.model.filter.search.AnalyticsFieldFilter; +import io.meeds.analytics.utils.AnalyticsUtils; import io.meeds.common.ContainerTransactional; import jakarta.annotation.PostConstruct; @@ -455,6 +457,65 @@ public void addUIWatcher(StatisticWatcher uiWatcher) { uiWatchers.add(uiWatcher); } + @Override + public List getSamples(List operations, + String fieldName, + List fieldValues, + String sortBy, + String sortDirection, + int limit) { + AnalyticsFilter searchFilter = new AnalyticsFilter(); + ArrayList filters = new ArrayList<>(); + searchFilter.setFilters(filters); + if (CollectionUtils.isNotEmpty(operations)) { + searchFilter.addInSetFilter(AnalyticsUtils.FIELD_OPERATION, operations.toArray(new String[operations.size()])); + } + if (StringUtils.isNotEmpty(fieldName) && CollectionUtils.isNotEmpty(fieldValues)) { + searchFilter.addInSetFilter(fieldName, fieldValues.toArray(new String[fieldValues.size()])); + } + searchFilter.addXAxisAggregation(new AnalyticsAggregation(AnalyticsAggregationType.TERMS, + sortBy, + sortDirection, + null, + limit)); + searchFilter.setLimit(limit); + return retrieveData(searchFilter); + } + + @Override + public ChartDataList getChart(List operations, + String fieldName, + List fieldValues, + String xAggregationField, + AnalyticsAggregationType xAggregationType, + String xAggregationSortDirection, + String yAggregationField, + AnalyticsAggregationType yAggregationType, + String yAggregationSortDirection, + int limit) { + AnalyticsFilter searchFilter; + searchFilter = new AnalyticsFilter(); + searchFilter.setFilters(new ArrayList<>()); + if (CollectionUtils.isNotEmpty(operations)) { + searchFilter.addInSetFilter(AnalyticsUtils.FIELD_OPERATION, operations.toArray(new String[operations.size()])); + } + if (StringUtils.isNotEmpty(fieldName) && CollectionUtils.isNotEmpty(fieldValues)) { + searchFilter.addInSetFilter(fieldName, fieldValues.toArray(new String[fieldValues.size()])); + } + searchFilter.addXAxisAggregation(new AnalyticsAggregation(xAggregationType, + xAggregationField, + xAggregationSortDirection, + null, + limit)); + searchFilter.setYAxisAggregation(new AnalyticsAggregation(yAggregationType, + yAggregationField, + yAggregationSortDirection, + null, + 1)); + searchFilter.setHideLabel(true); + return computeChartData(searchFilter); + } + private List buildFieldValuesResponse(String jsonResponse) throws JSONException { JSONObject json = new JSONObject(jsonResponse); JSONObject aggregations = json.has(AGGREGATIONS_RESPONSE_BODY) ? json.getJSONObject(AGGREGATIONS_RESPONSE_BODY) : null; @@ -789,9 +850,13 @@ private void appendSortQuery(StringBuilder esQuery, AnalyticsAggregationType aggregationType) { if (aggregationType.isUseSort()) { String sortField = null; + String sortDirection = aggregation.getSortDirection(); if ((i + 1) < aggregationsSize) { AnalyticsAggregation nextAggregation = aggregations.get(i + 1); sortField = getSortField(nextAggregation); + if (nextAggregation != null) { + sortDirection = nextAggregation.getSortDirection(); + } } else if (aggregationType == AnalyticsAggregationType.TERMS) { sortField = "_count"; } @@ -800,7 +865,7 @@ private void appendSortQuery(StringBuilder esQuery, , "order": {"$sortField": "$sortDirection"} """.replace(SORT_FIELD_REQUEST_BODY_PARAM, sortField) - .replace(SORT_DIRECTION_REQUEST_BODY_PARAM, aggregation.getSortDirection())); + .replace(SORT_DIRECTION_REQUEST_BODY_PARAM, sortDirection)); } } } diff --git a/analytics-services/src/main/java/io/meeds/analytics/rest/AnalyticsChartRest.java b/analytics-services/src/main/java/io/meeds/analytics/rest/AnalyticsChartRest.java new file mode 100644 index 000000000..8179db7f0 --- /dev/null +++ b/analytics-services/src/main/java/io/meeds/analytics/rest/AnalyticsChartRest.java @@ -0,0 +1,122 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package io.meeds.analytics.rest; + +import java.util.Collections; +import java.util.List; + +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import io.meeds.analytics.api.service.AnalyticsService; +import io.meeds.analytics.model.chart.ChartAggregationResult; +import io.meeds.analytics.model.chart.ChartDataList; +import io.meeds.analytics.model.filter.aggregation.AnalyticsAggregationType; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; + +@RestController +@RequestMapping("chart") +@Tag(name = "/analytics/rest/chart", description = "Retrieve Analytics Chart Data") +public class AnalyticsChartRest { + + @Autowired + private AnalyticsService analyticsService; + + @GetMapping + @Secured("analytics") + @Operation(summary = "Retrieve Analytics two dimensions Chat Data", + method = "GET", + description = "This will compute a two dimensions matrix of data useful for chart display") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + }) + public List getChart( + @Parameter(description = "Collected Statistic Operations to consider in query", + required = false) + @RequestParam(name = "operation", + required = false) + List operations, + @Parameter(description = "Filtered field name", + required = false) + @RequestParam(name = "fieldName", + required = false) + String fieldName, + @Parameter(description = "Filtered field values", + required = false) + @RequestParam(name = "fieldValue", + required = false) + List fieldValues, + @Parameter(description = "X Axis aggregation field", + required = true) + @RequestParam("xAggregationField") + String xAggregationField, + @Parameter(description = "X Axis aggregation type: MAX, MIN, SUM, COUNT, CARDINALITY (COUNT Distinct), TERMS, DATA or HISTOGRAM", + required = true) + @RequestParam("xAggregationType") + AnalyticsAggregationType xAggregationType, + @Parameter(description = "X Axis sort direction: asc or desc", + required = false) + @RequestParam(name = "xAggregationSortDirection", + required = false) + String xAggregationSortDirection, + @Parameter(description = "Y Axis aggregation field", + required = true) + @RequestParam("yAggregationField") + String yAggregationField, + @Parameter(description = "Y Axis aggregation type: MAX, MIN, SUM, COUNT, CARDINALITY (COUNT Distinct), TERMS, DATA or HISTOGRAM", + required = true) + @RequestParam("yAggregationType") + AnalyticsAggregationType yAggregationType, + @Parameter(description = "Y Axis sort direction: asc or desc", + required = true) + @RequestParam(name = "yAggregationSortDirection", + required = false) + String yAggregationSortDirection, + @Parameter(description = "Limit of x Axis results to retrieve", + required = true) + @RequestParam("limit") + int limit) { + ChartDataList chartData = analyticsService.getChart(operations, + fieldName, + fieldValues, + xAggregationField, + xAggregationType, + xAggregationSortDirection, + yAggregationField, + yAggregationType, + yAggregationSortDirection, + limit); + if (chartData != null && CollectionUtils.size(chartData.getCharts()) == 1) { + return chartData.getCharts().getFirst().getAggregationResults(); + } else { + return Collections.emptyList(); + } + } + +} diff --git a/analytics-services/src/main/java/io/meeds/analytics/rest/AnalyticsSampleRest.java b/analytics-services/src/main/java/io/meeds/analytics/rest/AnalyticsSampleRest.java new file mode 100644 index 000000000..3579ebc21 --- /dev/null +++ b/analytics-services/src/main/java/io/meeds/analytics/rest/AnalyticsSampleRest.java @@ -0,0 +1,91 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package io.meeds.analytics.rest; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import io.meeds.analytics.api.service.AnalyticsService; +import io.meeds.analytics.model.StatisticData; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; + +@RestController +@RequestMapping("samples") +@Tag(name = "samples", description = "Retrieve Analytics Samples") +public class AnalyticsSampleRest { + + @Autowired + private AnalyticsService analyticsService; + + @GetMapping + @Secured("analytics") + @Operation(summary = "Retrieve Analytics Samples", + method = "GET", + description = "This will allow for an administrator to retrieve collected statistics without user ACL retrictions") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + }) + public List getSamples( + @Parameter(description = "Collected Statistic Operations to consider in query", + required = false) + @RequestParam(name = "operation", + required = false) + List operations, + @Parameter(description = "Filtered field name", + required = false) + @RequestParam(name = "fieldName", + required = false) + String fieldName, + @Parameter(description = "Filtered field values", + required = false) + @RequestParam(name = "fieldValue", + required = false) + List fieldValues, + @Parameter(description = "Sort field name", + required = true) + @RequestParam("sortBy") + String sortBy, + @Parameter(description = "Sort direction: asc or desc", + required = true) + @RequestParam("sortDirection") + String sortDirection, + @Parameter(description = "Limit of samples to retrieve", + required = true) + @RequestParam("limit") + int limit) { + return analyticsService.getSamples(operations, + fieldName, + fieldValues, + sortBy, + sortDirection, + limit); + } + +} diff --git a/analytics-webapps/src/main/resources/locale/portlet/Analytics_en.properties b/analytics-webapps/src/main/resources/locale/portlet/Analytics_en.properties index 44db54802..16867d30b 100644 --- a/analytics-webapps/src/main/resources/locale/portlet/Analytics_en.properties +++ b/analytics-webapps/src/main/resources/locale/portlet/Analytics_en.properties @@ -256,6 +256,7 @@ analytics.spaceCreated=Space created analytics.click=Click analytics.createProfile=Create profile analytics.AppLauncherMenu=App launcher menu +analytics.pinActivity=Pin activity analytics.createActivity=Create activity analytics.deleteActivity=Delete activity analytics.AppLauncherClickViewAll=App launcher click view all @@ -450,3 +451,4 @@ analytics.settings.edit.button=Edit Settings analytics.jsonSettings.drawer.title=Edit JSON Settings analytics.jsonSettings.edit.button=Edit JSON analytics.cancel=Cancel +analytics.spaceTemplateId=Space Template Identifier diff --git a/analytics-webapps/src/main/resources/locale/portlet/social/SpacesAdministrationPortlet_en.properties b/analytics-webapps/src/main/resources/locale/portlet/social/SpacesAdministrationPortlet_en.properties new file mode 100644 index 000000000..9be3c8c00 --- /dev/null +++ b/analytics-webapps/src/main/resources/locale/portlet/social/SpacesAdministrationPortlet_en.properties @@ -0,0 +1,2 @@ +social.spaces.administration.manageSpaces.lastActivity=Last Activity +social.spaces.administration.manageSpaces.loadingLastActivity=Loading... diff --git a/analytics-webapps/src/main/webapp/WEB-INF/gatein-resources.xml b/analytics-webapps/src/main/webapp/WEB-INF/gatein-resources.xml index 02a9c6374..742aae061 100644 --- a/analytics-webapps/src/main/webapp/WEB-INF/gatein-resources.xml +++ b/analytics-webapps/src/main/webapp/WEB-INF/gatein-resources.xml @@ -257,4 +257,36 @@ + + analyticsManageSpacesExtension + SpacesAdministration + + + vue + + + vuetify + + + eXoVueI18n + + + extensionRegistry + + + analyticsApi + + + + + analyticsApi + + + diff --git a/analytics-webapps/src/main/webapp/vue-app/analytics-api/js/AnalyticsService.js b/analytics-webapps/src/main/webapp/vue-app/analytics-api/js/AnalyticsService.js new file mode 100644 index 000000000..4f4f21c49 --- /dev/null +++ b/analytics-webapps/src/main/webapp/vue-app/analytics-api/js/AnalyticsService.js @@ -0,0 +1,114 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +export const SPACE_ACTIVITY_OPERATIONS = [ + 'spaceCreated', + 'spaceRemoved', + 'spaceRenamed', + 'spaceDescriptionEdited', + 'spaceAccessEdited', + 'spaceRegistrationEdited', + 'spaceAvatarEdited', + 'spaceBannerEdited', + 'joined', + 'left', + 'pinActivity', + 'createActivity', + 'createComment', + 'likeActivity', + 'likeComment', + 'sendKudos', + 'taskCreated', + 'taskUpdated', + 'taskCommented', + 'taskTitleChanged', + 'taskDescriptionChanged', + 'taskStatusChanged', + 'taskCompleted', + 'noteCreated', + 'noteUpdated', + 'createRealization', + 'createRule', + 'exo.news.postArticle', +]; + +export function getSamples(options) { + const formData = new FormData(); + if (options.operations?.length) { + options.operations.forEach(o => formData.append('operation', o)); + } + if (options.fieldName) { + formData.append('fieldName', options.fieldName); + } + if (options.fieldValues?.length) { + options.fieldValues.forEach(v => formData.append('fieldValue', v)); + } + formData.append('sortBy', options.sortBy); + formData.append('sortDirection', options.sortDirection); + formData.append('limit', options.limit || 25); + const params = decodeURIComponent(new URLSearchParams(formData).toString()); + + return fetch(`/analytics/rest/samples?${params}`, { + method: 'GET', + credentials: 'include', + }).then(resp => { + if (!resp || !resp.ok) { + throw new Error('Response code indicates a server error', resp); + } else { + return resp.json(); + } + }); +} + +export function getChart(options) { + const formData = new FormData(); + if (options.operations?.length) { + options.operations.forEach(o => formData.append('operation', o)); + } + if (options.fieldName) { + formData.append('fieldName', options.fieldName); + } + if (options.fieldValues?.length) { + options.fieldValues.forEach(v => formData.append('fieldValue', v)); + } + + formData.append('xAggregationField', options.xAggregationField); + formData.append('xAggregationType', options.xAggregationType?.toUpperCase()); + if (options.xAggregationSortDirection) { + formData.append('xAggregationSortDirection', options.xAggregationSortDirection); + } + + formData.append('yAggregationField', options.yAggregationField); + formData.append('yAggregationType', options.yAggregationType?.toUpperCase()); + if (options.yAggregationSortDirection) { + formData.append('yAggregationSortDirection', options.yAggregationSortDirection); + } + formData.append('limit', options.limit || 25); + const params = decodeURIComponent(new URLSearchParams(formData).toString()); + return fetch(`/analytics/rest/chart?${params}`, { + method: 'GET', + credentials: 'include', + }).then(resp => { + if (!resp || !resp.ok) { + throw new Error('Response code indicates a server error', resp); + } else { + return resp.json(); + } + }); +} diff --git a/analytics-webapps/src/main/webapp/vue-app/analytics-api/main.js b/analytics-webapps/src/main/webapp/vue-app/analytics-api/main.js new file mode 100644 index 000000000..97d950612 --- /dev/null +++ b/analytics-webapps/src/main/webapp/vue-app/analytics-api/main.js @@ -0,0 +1,20 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import './services.js'; diff --git a/analytics-webapps/src/main/webapp/vue-app/analytics-api/services.js b/analytics-webapps/src/main/webapp/vue-app/analytics-api/services.js new file mode 100644 index 000000000..39833c31b --- /dev/null +++ b/analytics-webapps/src/main/webapp/vue-app/analytics-api/services.js @@ -0,0 +1,25 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2023 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as analyticsService from './js/AnalyticsService.js'; + +if (!Vue.prototype.$analyticsService) { + window.Object.defineProperty(Vue.prototype, '$analyticsService', { + value: analyticsService, + }); +} \ No newline at end of file diff --git a/analytics-webapps/src/main/webapp/vue-app/common-components/components/settings/form/SpaceFieldFilter.vue b/analytics-webapps/src/main/webapp/vue-app/common-components/components/settings/form/SpaceFieldFilter.vue index cf63b3db7..ae18627b2 100644 --- a/analytics-webapps/src/main/webapp/vue-app/common-components/components/settings/form/SpaceFieldFilter.vue +++ b/analytics-webapps/src/main/webapp/vue-app/common-components/components/settings/form/SpaceFieldFilter.vue @@ -24,6 +24,10 @@ v-model="selectedSpace" :labels="suggesterLabels" :ignore-items="spaceIds" + :search-options="{ + currentUser: '', + filterType: 'all', + }" include-spaces class="analytics-suggester" sugester-class="my-0" diff --git a/analytics-webapps/src/main/webapp/vue-app/spaces-administration-extension/components/SpacesAdministrationLastActivity.vue b/analytics-webapps/src/main/webapp/vue-app/spaces-administration-extension/components/SpacesAdministrationLastActivity.vue new file mode 100644 index 000000000..c98966ebb --- /dev/null +++ b/analytics-webapps/src/main/webapp/vue-app/spaces-administration-extension/components/SpacesAdministrationLastActivity.vue @@ -0,0 +1,101 @@ + + + diff --git a/analytics-webapps/src/main/webapp/vue-app/spaces-administration-extension/extensions.js b/analytics-webapps/src/main/webapp/vue-app/spaces-administration-extension/extensions.js new file mode 100644 index 000000000..6d24f7a45 --- /dev/null +++ b/analytics-webapps/src/main/webapp/vue-app/spaces-administration-extension/extensions.js @@ -0,0 +1,78 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +extensionRegistry.registerExtension('spaces-administration', 'table-column', { + rank: 10, + name: 'last-activity', + titleKey: 'social.spaces.administration.manageSpaces.lastActivity', + header: { + value: 'lastActivityTime', + align: 'center', + sortable: true, + class: 'space-last-activity-header px-1', + width: '150px', + }, + componentName: 'spaces-administration-last-activity', + sortBy: 'lastActivityTime', + customSort: async ({ + offset, + limit, + templateId, + expand, + sortDesc, + currentSpaces, + }) => { + const spaces = offset ? currentSpaces?.slice() : []; + const aggregationResults = await Vue.prototype + .$analyticsService + .getChart({ + operations: Vue.prototype.$analyticsService.SPACE_ACTIVITY_OPERATIONS, + fieldName: templateId && 'spaceTemplateId' || null, + fieldValues: templateId && [templateId] || null, + xAggregationField: 'spaceId', + xAggregationType: 'TERMS', + xAggregationSortDirection: null, + yAggregationField: 'timestamp', + yAggregationType: 'MAX', + yAggregationSortDirection: sortDesc ? 'desc' : 'asc', + limit: offset + limit * 2 + }); + const spaceIds = aggregationResults?.map(g => g.label) || []; + const spacesResult = await Promise.all( + spaceIds + .filter(id => Number(id)) + .slice(offset, offset + limit * 2) + .map(id => Vue.prototype.$spaceService.getSpaceById(id, expand, true).catch(() => {/* Space could be deleted */})) + ); + const validSpacesResult = spacesResult.filter(s => s).slice(0, limit); + validSpacesResult.forEach(s => spaces.push(s)); + if (validSpacesResult.length === 0) { + const noActivitySpaces = await Vue.prototype.$spaceService.getSpacesByFilter({ + offset: 0, + limit: offset + limit, + templateId, + expand, + sortBy: 'title', + sortDirection: 'asc', + }); + noActivitySpaces.spaces.filter(s => !spaces.find(s2 => s2.id === s.id)).forEach(s => spaces.push(s)); + } + return spaces; + }, +}); diff --git a/analytics-webapps/src/main/webapp/vue-app/spaces-administration-extension/initComponents.js b/analytics-webapps/src/main/webapp/vue-app/spaces-administration-extension/initComponents.js new file mode 100644 index 000000000..09ed489c2 --- /dev/null +++ b/analytics-webapps/src/main/webapp/vue-app/spaces-administration-extension/initComponents.js @@ -0,0 +1,27 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import SpacesAdministrationLastActivity from './components/SpacesAdministrationLastActivity.vue'; + +const components = { + 'spaces-administration-last-activity': SpacesAdministrationLastActivity, +}; + +for (const key in components) { + Vue.component(key, components[key]); +} \ No newline at end of file diff --git a/analytics-webapps/src/main/webapp/vue-app/spaces-administration-extension/main.js b/analytics-webapps/src/main/webapp/vue-app/spaces-administration-extension/main.js new file mode 100644 index 000000000..ce8be12de --- /dev/null +++ b/analytics-webapps/src/main/webapp/vue-app/spaces-administration-extension/main.js @@ -0,0 +1,21 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import './initComponents.js'; +import './extensions.js'; diff --git a/analytics-webapps/webpack.prod.js b/analytics-webapps/webpack.prod.js index 9dc2117d0..ac81ef758 100644 --- a/analytics-webapps/webpack.prod.js +++ b/analytics-webapps/webpack.prod.js @@ -48,6 +48,8 @@ const config = { analyticsRate:'./src/main/webapp/vue-app/rate-portlet/main.js', analyticsTable:'./src/main/webapp/vue-app/table-portlet/main.js', spacesListWidget:'./src/main/webapp/vue-app/spaces-list-widget/main.js', + spacesAdministrationExtension:'./src/main/webapp/vue-app/spaces-administration-extension/main.js', + analyticsApi:'./src/main/webapp/vue-app/analytics-api/main.js', }, output: { path: path.join(__dirname, 'target/analytics/'),