From 767de67923065b5d35de755851e59fc545fa89c0 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Thu, 23 Jan 2025 15:09:44 +0100 Subject: [PATCH 01/10] FE: impl of report exporter to settings --- web-app/packages/admin-lib/components.d.ts | 1 + .../admin-lib/src/modules/admin/adminApi.ts | 22 ++++- .../admin/components/ReportDownloadDialog.vue | 90 +++++++++++++++++++ .../admin-lib/src/modules/admin/store.ts | 38 +++++++- .../admin-lib/src/modules/admin/types.ts | 5 ++ .../admin/views/SettingsViewTemplate.vue | 28 +++++- .../components/app-settings/AppSettings.vue | 1 + .../app-settings/AppSettingsItem.vue | 4 +- web-app/packages/lib/src/common/date_utils.ts | 10 +++ 9 files changed, 192 insertions(+), 7 deletions(-) create mode 100644 web-app/packages/admin-lib/src/modules/admin/components/ReportDownloadDialog.vue diff --git a/web-app/packages/admin-lib/components.d.ts b/web-app/packages/admin-lib/components.d.ts index 89889c60..bf16d936 100644 --- a/web-app/packages/admin-lib/components.d.ts +++ b/web-app/packages/admin-lib/components.d.ts @@ -9,6 +9,7 @@ declare module 'vue' { export interface GlobalComponents { PAvatar: typeof import('primevue/avatar')['default'] PButton: typeof import('primevue/button')['default'] + PCalendar: typeof import('primevue/calendar')['default'] PColumn: typeof import('primevue/column')['default'] PDataTable: typeof import('primevue/datatable')['default'] PDivider: typeof import('primevue/divider')['default'] diff --git a/web-app/packages/admin-lib/src/modules/admin/adminApi.ts b/web-app/packages/admin-lib/src/modules/admin/adminApi.ts index 6380ef6c..08114297 100644 --- a/web-app/packages/admin-lib/src/modules/admin/adminApi.ts +++ b/web-app/packages/admin-lib/src/modules/admin/adminApi.ts @@ -18,7 +18,8 @@ import { CreateUserData, PaginatedAdminProjectsResponse, PaginatedAdminProjectsParams, - ServerUsageResponse + ServerUsageResponse, + DownloadReportParams } from '@/modules/admin/types' export const AdminApi = { @@ -99,6 +100,23 @@ export const AdminApi = { }, async getServerUsage(): Promise> { - return AdminModule.httpService.get('/app/admin/usage', ) + return AdminModule.httpService.get('/app/admin/usage') + }, + + /** + * Create url for csv file with server statistics + */ + contructDownloadStatisticsUrl(): string { + return AdminModule.httpService.absUrl('/app/admin/report') + }, + + async downloadStatistics( + url: string, + params: DownloadReportParams + ): Promise> { + return AdminModule.httpService.get(url, { + responseType: 'blob', + params + }) } } diff --git a/web-app/packages/admin-lib/src/modules/admin/components/ReportDownloadDialog.vue b/web-app/packages/admin-lib/src/modules/admin/components/ReportDownloadDialog.vue new file mode 100644 index 00000000..a092d9cf --- /dev/null +++ b/web-app/packages/admin-lib/src/modules/admin/components/ReportDownloadDialog.vue @@ -0,0 +1,90 @@ + + + diff --git a/web-app/packages/admin-lib/src/modules/admin/store.ts b/web-app/packages/admin-lib/src/modules/admin/store.ts index 7b9281ff..46b3e9af 100644 --- a/web-app/packages/admin-lib/src/modules/admin/store.ts +++ b/web-app/packages/admin-lib/src/modules/admin/store.ts @@ -13,6 +13,7 @@ import { useNotificationStore, UserResponse } from '@mergin/lib' +import FileSaver from 'file-saver' import { defineStore, getActivePinia } from 'pinia' import Cookies from 'universal-cookie' @@ -22,10 +23,12 @@ import { AdminApi } from '@/modules/admin/adminApi' import { LatestServerVersionResponse, PaginatedAdminProjectsParams, - PaginatedAdminProjectsResponse, ServerUsageResponse, + PaginatedAdminProjectsResponse, + ServerUsageResponse, UpdateUserPayload, UsersResponse } from '@/modules/admin/types' +import axios from 'axios' export interface AdminState { loading: boolean @@ -325,6 +328,39 @@ export const useAdminStore = defineStore('adminModule', { } catch (e) { notificationStore.error({ text: errorUtils.getErrorMessage(e) }) } + }, + + async downloadReport(payload: { from: Date; to: Date }) { + const notificationStore = useNotificationStore() + + try { + const { from, to } = payload + const url = AdminApi.contructDownloadStatisticsUrl() + const date = new Date() + // we need to sanitize hours from custom range in Calendar picker, because it's 00:00:00 by default. If you convert it to UTC -> it could be previous day, which is not what you selected. + from.setHours(date.getHours(), date.getMinutes(), date.getSeconds()) + to.setHours(date.getHours(), date.getMinutes(), date.getSeconds()) + // toISOString converts date to iso format and UTC zone + const date_from = payload.from.toISOString().split('T')[0] + const date_to = payload.to.toISOString().split('T')[0] + const resp = await AdminApi.downloadStatistics(url, { + date_from, + date_to + }) + const fileName = + resp.headers['content-disposition'].split('filename=')[1] + FileSaver.saveAs(resp.data, fileName) + } catch (e) { + // parse error details from blob + if (axios.isAxiosError(e)) { + let resp + const blob = new Blob([e.response.data], { type: 'text/plain' }) + blob.text().then((text) => { + resp = JSON.parse(text) + notificationStore.error({ text: resp.detail }) + }) + } + } } } }) diff --git a/web-app/packages/admin-lib/src/modules/admin/types.ts b/web-app/packages/admin-lib/src/modules/admin/types.ts index 2c3bc54d..e443b0a7 100644 --- a/web-app/packages/admin-lib/src/modules/admin/types.ts +++ b/web-app/packages/admin-lib/src/modules/admin/types.ts @@ -80,4 +80,9 @@ export interface ServerUsage { editors: number } +export interface DownloadReportParams { + date_from: string + date_to: string +} + /* eslint-enable camelcase */ diff --git a/web-app/packages/admin-lib/src/modules/admin/views/SettingsViewTemplate.vue b/web-app/packages/admin-lib/src/modules/admin/views/SettingsViewTemplate.vue index ee4880c7..5ef98bf2 100644 --- a/web-app/packages/admin-lib/src/modules/admin/views/SettingsViewTemplate.vue +++ b/web-app/packages/admin-lib/src/modules/admin/views/SettingsViewTemplate.vue @@ -16,12 +16,19 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial - + + + @@ -33,9 +40,11 @@ import { AppContainer, AppSection, AppSettings, - AppSettingsItemConfig + AppSettingsItemConfig, + useDialogStore } from '@mergin/lib' +import ReportDownloadDialog from '../components/ReportDownloadDialog.vue' import { useAdminStore } from '../store' import AdminLayout from '@/modules/admin/components/AdminLayout.vue' @@ -46,16 +55,31 @@ withDefaults(defineProps<{ settingsItems?: AppSettingsItemConfig[] }>(), { title: 'Check for updates', description: 'Let Mergin Maps automatically check for new updates', key: 'checkForUpdates' + }, + { + title: 'Server usage report', + description: 'Download usage statistics for your server deployment.', + key: 'downloadReport' } ] }) const adminStore = useAdminStore() +const dialogStore = useDialogStore() function switchCheckForUpdates() { const value = !adminStore.checkForUpdates adminStore.setCheckUpdatesToCookies({ value }) } + +function downloadReport() { + dialogStore.show({ + component: ReportDownloadDialog, + params: { + dialog: { header: 'Doownload usage report' } + } + }) +} diff --git a/web-app/packages/lib/src/common/components/app-settings/AppSettings.vue b/web-app/packages/lib/src/common/components/app-settings/AppSettings.vue index 4627492d..29cd1800 100644 --- a/web-app/packages/lib/src/common/components/app-settings/AppSettings.vue +++ b/web-app/packages/lib/src/common/components/app-settings/AppSettings.vue @@ -5,6 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial -->