Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server usage in admin #334

Merged
merged 6 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions server/mergin/auth/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,28 @@ paths:
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFoundResp"
/app/admin/usage:
get:
tags:
- user
- admin
- project
summary: Get server usage statistics
description: List server storage, projects, active contributors for admin
operationId: mergin.auth.controller.get_server_usage
responses:
"200":
description: Server usage details
content:
application/json:
schema:
$ref: "#/components/schemas/ServerUsage"
"400":
$ref: "#/components/responses/BadStatusResp"
"401":
$ref: "#/components/responses/UnauthorizedError"
"403":
$ref: "#/components/responses/Forbidden"
/app/auth/login:
post:
summary: Login
Expand Down Expand Up @@ -845,3 +867,28 @@ components:
- reader
- guest
example: reader
ServerUsage:
type: object
properties:
active_monthly_contributors:
type: array
description: count of users who made a project change last months
items:
type: integer
example: 2
projects:
type: integer
description: total number of projects
example: 12
storage:
type: string
description: projest files size in bytes
example: 1024 kB
users:
type: integer
description: count of registered accounts
example: 6
workspaces:
type: integer
description: number of workspaces on the server
example: 3
24 changes: 21 additions & 3 deletions server/mergin/auth/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pytz
from datetime import datetime, timedelta
from connexion import NoContent
from sqlalchemy import func, desc, asc, or_
from sqlalchemy import func, desc, asc
from sqlalchemy.sql.operators import is_
from flask import request, current_app, jsonify, abort, render_template
from flask_login import login_user, logout_user, current_user
Expand Down Expand Up @@ -35,8 +35,9 @@
UserChangePasswordForm,
ApiLoginForm,
)
from ..app import DEPRECATION_API_MSG, db
from ..utils import format_time_delta
from ..app import db
from ..sync.models import Project
from ..sync.utils import files_size


# public endpoints
Expand Down Expand Up @@ -498,3 +499,20 @@ def get_user_info():
for inv in invitations
]
return user_info, 200


@auth_required(permissions=["admin"])
def get_server_usage():
data = {
"active_monthly_contributors": [
current_app.ws_handler.monthly_contributors_count(),
current_app.ws_handler.monthly_contributors_count(month_offset=1),
current_app.ws_handler.monthly_contributors_count(month_offset=2),
current_app.ws_handler.monthly_contributors_count(month_offset=3),
],
"projects": Project.query.count(),
"storage": files_size(),
"users": User.query.count(),
"workspaces": current_app.ws_handler.workspace_count(),
}
return data, 200
1 change: 0 additions & 1 deletion server/mergin/sync/public_api_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from typing import Dict
from urllib.parse import quote
import uuid
from time import time
from datetime import datetime
import psycopg2
from blinker import signal
Expand Down
35 changes: 34 additions & 1 deletion server/mergin/sync/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
import secrets
from threading import Timer
from uuid import UUID
from connexion import NoContent
from shapely import wkb
from shapely.errors import ShapelyError
from gevent import sleep
from flask import Request
from typing import Optional
from sqlalchemy import text


def generate_checksum(file, chunk_size=4096):
Expand Down Expand Up @@ -304,3 +304,36 @@ def split_project_path(project_path):
def get_device_id(request: Request) -> Optional[str]:
"""Get device uuid from http header X-Device-Id"""
return request.headers.get("X-Device-Id")


def files_size():
"""Get total size of all files"""
from mergin.app import db

files_size = text(
f"""
WITH partials AS (
WITH latest_files AS (
SELECT distinct unnest(file_history_ids) AS file_id
FROM latest_project_files pf
)
SELECT
SUM(size)
FROM file_history
WHERE change = 'create'::push_change_type OR change = 'update'::push_change_type
UNION
SELECT
SUM(COALESCE((diff ->> 'size')::bigint, 0))
FROM file_history
WHERE change = 'update_diff'::push_change_type
UNION
SELECT
SUM(size)
FROM latest_files lf
LEFT OUTER JOIN file_history fh ON fh.id = lf.file_id
WHERE fh.change = 'update_diff'::push_change_type
)
SELECT pg_size_pretty(SUM(sum)) FROM partials;
"""
)
return db.session.execute(files_size).scalar()
4 changes: 2 additions & 2 deletions server/mergin/sync/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,15 +255,15 @@ def workspace_count():
return 1

@staticmethod
def monthly_contributors_count():
def monthly_contributors_count(month_offset=0):
today = datetime.now(timezone.utc)
year = today.year
month = today.month
return (
db.session.query(ProjectVersion.author_id)
.filter(
extract("year", ProjectVersion.created) == year,
extract("month", ProjectVersion.created) == month,
extract("month", ProjectVersion.created) == month - month_offset,
)
.group_by(ProjectVersion.author_id)
.count()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Migrage project version author name to user.id

Revision ID: 1ab5b02ce532
Revises: 57d0de13ce4a
Revises: 1c23e3be03a3
MarcelGeo marked this conversation as resolved.
Show resolved Hide resolved
Create Date: 2024-09-06 14:01:40.668483

"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ import { useRoute } from 'vue-router'
const route = useRoute()

const sidebarItems = computed<SideBarItemModel[]>(() => [
{
title: 'Overview',
to: '/overview',
icon: 'ti ti-layout-dashboard',
active: route.matched.some((item) => item.name === AdminRoutes.OVERVIEW)
},
{
title: 'Accounts',
to: '/accounts',
Expand Down
15 changes: 13 additions & 2 deletions web-app/packages/admin-app/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
ProjectFilesView,
ProjectSettingsView,
ProjectVersionView,
ProjectVersionsView
ProjectVersionsView,
OverviewView
} from '@mergin/admin-lib'
import {
NotFoundView,
Expand Down Expand Up @@ -48,7 +49,7 @@ export const createRouter = (pinia: Pinia) => {
{
path: '/',
name: 'admin',
redirect: '/accounts'
redirect: '/overview'
},
{
path: '/accounts',
Expand Down Expand Up @@ -144,6 +145,16 @@ export const createRouter = (pinia: Pinia) => {
header: AppHeader
},
props: true
},
{
path: '/overview',
MarcelGeo marked this conversation as resolved.
Show resolved Hide resolved
name: AdminRoutes.OVERVIEW,
components: {
default: OverviewView,
sidebar: Sidebar,
header: AppHeader
},
props: true
}
]
})
Expand Down
1 change: 1 addition & 0 deletions web-app/packages/admin-lib/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ declare module 'vue' {
PInputSwitch: typeof import('primevue/inputswitch')['default']
PInputText: typeof import('primevue/inputtext')['default']
PPassword: typeof import('primevue/password')['default']
PProgressBar: typeof import('primevue/progressbar')['default']
PTabPanel: typeof import('primevue/tabpanel')['default']
PTabView: typeof import('primevue/tabview')['default']
RouterLink: typeof import('vue-router')['RouterLink']
Expand Down
10 changes: 6 additions & 4 deletions web-app/packages/admin-lib/src/modules/admin/adminApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial

import {
ApiRequestSuccessInfo,
errorUtils,
LoginData,
PaginatedUsersParams,
UserProfileResponse,
Expand All @@ -14,13 +12,13 @@ import { AxiosResponse } from 'axios'

import { AdminModule } from '@/modules/admin/module'
import {
UsersParams,
UpdateUserData,
UsersResponse,
LatestServerVersionResponse,
CreateUserData,
PaginatedAdminProjectsResponse,
PaginatedAdminProjectsParams
PaginatedAdminProjectsParams,
ServerUsageResponse
} from '@/modules/admin/types'

export const AdminApi = {
Expand Down Expand Up @@ -98,5 +96,9 @@ export const AdminApi = {
`/app/project/removed-project/restore/${id}`,
null
)
},

async getServerUsage(): Promise<AxiosResponse<ServerUsageResponse>> {
return AdminModule.httpService.get('/app/admin/usage', )
}
}
1 change: 1 addition & 0 deletions web-app/packages/admin-lib/src/modules/admin/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { RouteRecord } from 'vue-router'
export enum AdminRoutes {
ACCOUNTS = 'accounts',
ACCOUNT = 'account',
OVERVIEW = 'overview',
PROJECTS = 'projects',
PROJECT = 'project',
SETTINGS = 'settings',
Expand Down
15 changes: 14 additions & 1 deletion web-app/packages/admin-lib/src/modules/admin/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { AdminApi } from '@/modules/admin/adminApi'
import {
LatestServerVersionResponse,
PaginatedAdminProjectsParams,
PaginatedAdminProjectsResponse,
PaginatedAdminProjectsResponse, ServerUsageResponse,
UpdateUserPayload,
UsersResponse
} from '@/modules/admin/types'
Expand Down Expand Up @@ -112,6 +112,9 @@ export const useAdminStore = defineStore('adminModule', {
setIsServerConfigHidden(value: boolean) {
this.isServerConfigHidden = value
},
setUsage(data: ServerUsageResponse){
this.usage = data
},

async fetchUsers(payload: { params: PaginatedUsersParams }) {
const notificationStore = useNotificationStore()
Expand Down Expand Up @@ -313,6 +316,16 @@ export const useAdminStore = defineStore('adminModule', {
text: 'Unable to remove project'
})
}
},

async getServerUsage() {
const notificationStore = useNotificationStore()
try {
const response = await AdminApi.getServerUsage()
this.setUsage(response.data)
} catch (e) {
notificationStore.error({ text: errorUtils.getErrorMessage(e) })
}
}
}
})
11 changes: 11 additions & 0 deletions web-app/packages/admin-lib/src/modules/admin/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,15 @@ export type PaginatedAdminProjectsResponse =
export interface PaginatedAdminProjectsParams extends PaginatedRequestParams {
like?: string
}

export type ServerUsageResponse = ServerUsage

export interface ServerUsage {
active_monthly_contributors: number[]
projects: number
storage: string
users: number
workspaces: number
}

/* eslint-enable camelcase */
Loading
Loading