From 428c232ea9fbff7295127f1789da837ef6a012ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Thu, 26 Jan 2023 09:35:25 +0100 Subject: [PATCH 1/7] Active members with organizations --- .../database/repositories/memberRepository.ts | 32 ++++++++++++++----- .../repositories/types/memberTypes.ts | 1 + 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/backend/src/database/repositories/memberRepository.ts b/backend/src/database/repositories/memberRepository.ts index 866eb109da..279469c883 100644 --- a/backend/src/database/repositories/memberRepository.ts +++ b/backend/src/database/repositories/memberRepository.ts @@ -464,37 +464,52 @@ class MemberRepository { conditions.push("COALESCE((m.attributes->'isTeamMember'->'default')::boolean, false) = false") } + const activityConditions = ['1=1'] + if (filter.platforms && filter.platforms.length > 0) { - conditions.push('a.platform in (:platforms)') + activityConditions.push('platform in (:platforms)') parameters.platforms = filter.platforms } const conditionsString = conditions.join(' and ') + const activityConditionsString = activityConditions.join(' and ') const direction = orderBy.split('_')[1].toLowerCase() === 'desc' ? 'desc' : 'asc' let orderString: string if (orderBy.startsWith('activityCount')) { - orderString = `count(a.id) ${direction}` + orderString = `ad."activityCount" ${direction}` } else if (orderBy.startsWith('activeDaysCount')) { - orderString = `count(distinct a.timestamp::date) ${direction}` + orderString = `ad."activeDaysCount" ${direction}` } else { throw new Error(`Invalid order by: ${orderBy}`) } const limitCondition = `limit ${limit} offset ${offset}` const query = ` + with orgs as (select mo."memberId", json_agg(row_to_json(o.*)) as organizations + from "memberOrganizations" mo + inner join organizations o on mo."organizationId" = o.id + group by mo."memberId"), + activity_data as (select "memberId", + count(id) as "activityCount", + count(distinct timestamp::date) as "activeDaysCount" + from activities + where ${activityConditionsString} and + timestamp >= :periodStart and + timestamp < :periodEnd + group by "memberId") select m.id, m."displayName", m.username, m.attributes, - count(a.id) as "activityCount", - count(distinct a.timestamp::date) as "activeDaysCount", + ad."activityCount", + ad."activeDaysCount", + coalesce(o.organizations, json_build_array()) as organizations, count(*) over () as "totalCount" from members m - inner join activities a on (m.id = a."memberId" and a.timestamp >= :periodStart and - a.timestamp < :periodEnd) + inner join activity_data ad on ad."memberId" = m.id + left join orgs o on o."memberId" = m.id where ${conditionsString} - group by m.id, m."displayName", m.username, m.attributes order by ${orderString} ${limitCondition}; ` @@ -527,6 +542,7 @@ class MemberRepository { displayName: row.displayName, username: row.username, attributes: row.attributes, + organizations: row.organizations, activityCount: parseInt(row.activityCount, 10), activeDaysCount: parseInt(row.activeDaysCount, 10), } diff --git a/backend/src/database/repositories/types/memberTypes.ts b/backend/src/database/repositories/types/memberTypes.ts index 6fe41fae17..73497b2f3d 100644 --- a/backend/src/database/repositories/types/memberTypes.ts +++ b/backend/src/database/repositories/types/memberTypes.ts @@ -3,6 +3,7 @@ export interface IActiveMemberData { displayName: string username: any attributes: any + organizations: any[] activityCount: number activeDaysCount: number } From e4f10eb131422f2e9b0a1fec9eec5ba5bf514d29 Mon Sep 17 00:00:00 2001 From: Joana Maia Date: Wed, 18 Jan 2023 11:55:26 +0000 Subject: [PATCH 2/7] Report detailed drawers --- .../components/list/member-list-toolbar.vue | 2 +- frontend/src/modules/member/member-service.js | 38 ++ frontend/src/modules/member/store/actions.js | 27 +- .../templates/report-member-template.vue | 15 +- .../templates/template-report-charts.js | 10 +- frontend/src/modules/report/tooltip.js | 74 +++- .../components/v2/shared/widget-area.vue | 30 +- .../components/v2/shared/widget-drawer.vue | 339 ++++++++++++++++++ .../components/v2/shared/widget-empty.vue | 9 +- .../v2/shared/widget-members-table.vue | 61 +++- .../v2/widget-active-leaderboard-members.vue | 127 +++++-- .../v2/widget-active-members-area.vue | 101 +++++- .../components/v2/widget-active-members.vue | 130 ++++--- .../components/v2/widget-total-members.vue | 95 ++++- .../src/modules/widget/widget-constants.js | 7 + frontend/src/modules/widget/widget-queries.js | 129 +++++-- frontend/src/shared/drawer/drawer.vue | 2 +- frontend/src/utils/date.js | 7 +- frontend/src/utils/string.js | 4 + 19 files changed, 1083 insertions(+), 124 deletions(-) create mode 100644 frontend/src/modules/widget/components/v2/shared/widget-drawer.vue diff --git a/frontend/src/modules/member/components/list/member-list-toolbar.vue b/frontend/src/modules/member/components/list/member-list-toolbar.vue index 4568dcede0..dd565ed505 100644 --- a/frontend/src/modules/member/components/list/member-list-toolbar.vue +++ b/frontend/src/modules/member/components/list/member-list-toolbar.vue @@ -131,7 +131,7 @@ export default { async handleDoExport() { try { - await this.doExport(true) + await this.doExport({ selected: true }) } catch (error) { console.log(error) } diff --git a/frontend/src/modules/member/member-service.js b/frontend/src/modules/member/member-service.js index d453a0efd7..935103861b 100644 --- a/frontend/src/modules/member/member-service.js +++ b/frontend/src/modules/member/member-service.js @@ -127,6 +127,44 @@ export class MemberService { return response.data } + static async listActive({ + platform, + isTeamMember, + activityTimestampFrom, + activityTimestampTo, + orderBy, + offset, + limit + }) { + const params = { + ...(platform.length && { + 'filter[platforms]': platform + .map((p) => p.value) + .join(',') + }), + ...(isTeamMember === false && { + 'filter[isTeamMember]': isTeamMember + }), + 'filter[activityTimestampFrom]': + activityTimestampFrom, + 'filter[activityTimestampTo]': activityTimestampTo, + orderBy, + offset, + limit + } + + const tenantId = AuthCurrentTenant.get() + + const response = await authAxios.get( + `/tenant/${tenantId}/member/active`, + { + params + } + ) + + return response.data + } + static async listAutocomplete(query, limit) { const params = { query, diff --git a/frontend/src/modules/member/store/actions.js b/frontend/src/modules/member/store/actions.js index d23c72c6c0..d72a556ca8 100644 --- a/frontend/src/modules/member/store/actions.js +++ b/frontend/src/modules/member/store/actions.js @@ -13,15 +13,26 @@ export default { async doExport( { commit, getters, rootGetters, dispatch }, - selected = false + { + selected = false, + customIds = [], + customFilter = null + } ) { let filter + if (selected) { + const ids = customIds.length + ? customIds + : [getters.selectedRows.map((i) => i.id)] + filter = { id: { - in: [getters.selectedRows.map((i) => i.id)] + in: ids } } + } else if (customFilter) { + filter = customFilter } else { filter = getters.activeView.filter } @@ -52,8 +63,14 @@ export default { confirmButtonText: 'Send download link to e-mail', cancelButtonText: 'Cancel', badgeContent: selected - ? `${getters.selectedRows.length} member${ - getters.selectedRows.length === 1 ? '' : 's' + ? `${ + customIds.length || + getters.selectedRows.length + } member${ + (customIds.length || + getters.selectedRows.length) === 1 + ? '' + : 's' }` : `View: ${getters.activeView.label}`, highlightedInfo: `${tenantCsvExportCount}/${planCsvExportMax} exports available in this plan used` @@ -64,7 +81,7 @@ export default { getters.orderBy, 0, null, - !selected // build API payload if selected === false + !selected && !customFilter // build API payload if selected === false || !customFilter ) await dispatch(`auth/doRefreshCurrentUser`, null, { diff --git a/frontend/src/modules/report/pages/templates/report-member-template.vue b/frontend/src/modules/report/pages/templates/report-member-template.vue index c8b3d96cf0..a3145b9d68 100644 --- a/frontend/src/modules/report/pages/templates/report-member-template.vue +++ b/frontend/src/modules/report/pages/templates/report-member-template.vue @@ -6,9 +6,18 @@ class="app-page-spinner" >
- - - + + + { + return context[0].dataset.tooltipBtn + } } }, legend: { diff --git a/frontend/src/modules/report/tooltip.js b/frontend/src/modules/report/tooltip.js index ddcbe76c45..2146007aa9 100644 --- a/frontend/src/modules/report/tooltip.js +++ b/frontend/src/modules/report/tooltip.js @@ -1,6 +1,9 @@ -export const externalTooltipHandler = (context) => { +export const externalTooltipHandler = ( + context, + clickFn +) => { // Tooltip Element - const { tooltip } = context + const { tooltip, chart } = context let tooltipEl = document.getElementById('chartjs-tooltip') // Create element on first render if (!tooltipEl) { @@ -10,9 +13,46 @@ export const externalTooltipHandler = (context) => { document.body.appendChild(tooltipEl) } + // Handle mouseenter event on tooltip + tooltipEl.onmouseenter = () => { + if (chart.canvas) { + const meta = chart.getDatasetMeta(0) + const canvas = chart.canvas.getBoundingClientRect() + const point = + meta.data[ + tooltip.dataPoints[0].dataIndex + ].getCenterPoint() + const evt = new MouseEvent('mousemove', { + clientX: canvas.x + point.x, + clientY: canvas.y + point.y + }) + const canvasNode = chart.canvas + + // Dispatch mousemove event to canvas + // This will allow for the tooltip render + // logic to still be on the library side + canvasNode?.dispatchEvent(evt) + } + } + + // Handle mouseleave event on tooltip + tooltipEl.onmouseleave = ({ clientX, clientY }) => { + if (chart.canvas) { + const evt = new MouseEvent('mouseout', { + clientX, + clientY + }) + const canvasNode = chart.canvas + + // Dispatch mouseposition in the mouseout event + // This will hide tooltip + canvasNode?.dispatchEvent(evt) + } + } + // Hide if no tooltip if (tooltip.opacity === 0) { - tooltipEl.style.opacity = 0 + tooltipEl.style.display = 'none' return } @@ -57,7 +97,7 @@ export const externalTooltipHandler = (context) => { innerHtml += ` -
+
@@ -80,6 +120,24 @@ export const externalTooltipHandler = (context) => { innerHtml += '' + let footerBtn = document.getElementById( + 'custom-tooltip-footer-btn' + ) + if (!footerBtn && tooltip.footer) { + footerBtn = document.createElement('el-button') + footerBtn.id = 'custom-tooltip-footer-btn' + tooltip.footer.forEach((lines) => { + footerBtn.className = + 'btn btn--sm btn--full btn--secondary mt-4' + footerBtn.innerText = lines + tooltipEl.appendChild(footerBtn) + }) + } + + // Add clickFn to footerBtn + // This will allow each graph to handle the button click differently + footerBtn.onclick = clickFn + let tableRoot = tooltipEl.querySelector('table') tableRoot.innerHTML = innerHtml } @@ -89,6 +147,7 @@ export const externalTooltipHandler = (context) => { // Display, position, and set styles for font tooltipEl.style.opacity = 1 + tooltipEl.style.display = 'block' tooltipEl.style.backgroundColor = 'white' tooltipEl.style.borderRadius = '8px' tooltipEl.style.position = 'absolute' @@ -103,12 +162,13 @@ export const externalTooltipHandler = (context) => { tooltipEl.style.top = position.top + window.pageYOffset + - tooltip.caretY - + (tooltip.dataPoints?.[0]?.element?.y || + tooltip.caretY) - tooltipEl.getBoundingClientRect().height - - 40 + + 20 + 'px' tooltipEl.style.padding = '12px' tooltipEl.style.textAlign = 'left' - tooltipEl.style.pointerEvents = 'none' tooltipEl.style.zIndex = '20' + tooltipEl.style.maxWidth = '200px' } diff --git a/frontend/src/modules/widget/components/v2/shared/widget-area.vue b/frontend/src/modules/widget/components/v2/shared/widget-area.vue index d5c4627d98..a13824b0f8 100644 --- a/frontend/src/modules/widget/components/v2/shared/widget-area.vue +++ b/frontend/src/modules/widget/components/v2/shared/widget-area.vue @@ -16,12 +16,20 @@ export default { + + diff --git a/frontend/src/modules/widget/components/v2/shared/widget-empty.vue b/frontend/src/modules/widget/components/v2/shared/widget-empty.vue index b4ea75cccc..bde1096ba9 100644 --- a/frontend/src/modules/widget/components/v2/shared/widget-empty.vue +++ b/frontend/src/modules/widget/components/v2/shared/widget-empty.vue @@ -7,7 +7,10 @@
No data found
-
+
Try to select a different time period
@@ -21,6 +24,10 @@ const props = defineProps({ type: { type: String, default: 'chart' + }, + withDescription: { + type: Boolean, + default: true } }) diff --git a/frontend/src/modules/widget/components/v2/shared/widget-members-table.vue b/frontend/src/modules/widget/components/v2/shared/widget-members-table.vue index 6321b23525..b96c9fc1e6 100644 --- a/frontend/src/modules/widget/components/v2/shared/widget-members-table.vue +++ b/frontend/src/modules/widget/components/v2/shared/widget-members-table.vue @@ -1,7 +1,24 @@ diff --git a/frontend/src/modules/widget/components/v2/widget-active-members-area.vue b/frontend/src/modules/widget/components/v2/widget-active-members-area.vue index a6bde657d3..c76af97a36 100644 --- a/frontend/src/modules/widget/components/v2/widget-active-members-area.vue +++ b/frontend/src/modules/widget/components/v2/widget-active-members-area.vue @@ -53,10 +53,24 @@ ...chartOptions('area') }" :granularity="granularity.value" + @on-view-more-click="onViewMoreClick" />
+