From c47b364c07b091d413b80287fd5e3fc4a70a2fe3 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Fri, 13 Sep 2024 21:48:06 +0200 Subject: [PATCH 1/5] Improve searching + added reation date to User --- server/mergin/auth/api.yaml | 9 +++++++-- server/mergin/auth/controller.py | 9 +++++---- server/mergin/auth/schemas.py | 1 + 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/server/mergin/auth/api.yaml b/server/mergin/auth/api.yaml index 337dc862..f89e110d 100644 --- a/server/mergin/auth/api.yaml +++ b/server/mergin/auth/api.yaml @@ -171,7 +171,7 @@ paths: count: type: integer example: 10 - users: + items: type: array items: $ref: "#/components/schemas/User" @@ -670,6 +670,11 @@ components: type: string format: date-time example: 2023-07-30T08:47:58Z + registration_date: + nullable: true + type: string + format: date-time + example: 2023-07-30T08:47:58Z profile: $ref: "#/components/schemas/UserProfile" PaginatedUsers: @@ -839,4 +844,4 @@ components: - editor - reader - guest - example: reader \ No newline at end of file + example: reader diff --git a/server/mergin/auth/controller.py b/server/mergin/auth/controller.py index 36932def..42d84714 100644 --- a/server/mergin/auth/controller.py +++ b/server/mergin/auth/controller.py @@ -6,7 +6,7 @@ import pytz from datetime import datetime, timedelta from connexion import NoContent -from sqlalchemy import func, desc, asc +from sqlalchemy import func, desc, asc, or_ 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 @@ -454,8 +454,9 @@ def get_paginated_users( ) if like: - attr = User.email if "@" in like else User.username - users = users.filter(attr.ilike(f"%{like}%")) + users = users.filter( + User.username.ilike(f"%{like}%") | User.email.ilike(f"%{like}%") + ) if descending and order_by: users = users.order_by(desc(User.__table__.c[order_by])) @@ -467,7 +468,7 @@ def get_paginated_users( result_users = UserSchema(many=True).dump(result) - data = {"users": result_users, "total": total} + data = {"items": result_users, "count": total} return data, 200 diff --git a/server/mergin/auth/schemas.py b/server/mergin/auth/schemas.py index bcebca5c..10475b2c 100644 --- a/server/mergin/auth/schemas.py +++ b/server/mergin/auth/schemas.py @@ -71,6 +71,7 @@ class Meta: "verified_email", "profile", "scheduled_removal", + "registration_date", ) load_instance = True From 658f113bb0b148e036069acca373bbdf61e7bfd2 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Fri, 13 Sep 2024 21:48:56 +0200 Subject: [PATCH 2/5] Added: - AccountsTable / AccountDetail / banners / dialogs Refactored: - dl -> dd - label class removing --- web-app/packages/admin-app/src/router.ts | 38 +-- web-app/packages/admin-app/vite.config.ts | 1 + web-app/packages/admin-lib/components.d.ts | 6 + .../admin-lib/src/modules/admin/adminApi.ts | 31 +- .../admin/components/CreateUserForm.vue | 166 +++++----- .../admin-lib/src/modules/admin/store.ts | 27 +- .../admin-lib/src/modules/admin/types.ts | 5 +- .../modules/admin/views/AccountDetailView.vue | 286 +++++++++++++++++ .../src/modules/admin/views/AccountsView.vue | 261 ++++++++-------- .../src/modules/admin/views/ProfileView.vue | 290 ------------------ .../src/modules/admin/views/index.ts | 4 +- .../packages/admin-lib/src/shims-pinia.d.ts | 7 + web-app/packages/admin-lib/src/shims-tsx.d.ts | 15 - web-app/packages/admin-lib/src/shims-vue.d.ts | 15 +- web-app/packages/admin-lib/vite.config.ts | 1 + .../themes/mm-theme-light/_extensions.scss | 11 +- .../common/components/AppPasswordTooltip.vue | 2 +- .../lib/src/common/components/types.ts | 7 + web-app/packages/lib/src/mm-theme.ts | 26 +- .../project/components/FileDetailSidebar.vue | 12 +- .../project/components/ProjectShareDialog.vue | 2 +- .../components/ProjectShareDialogTemplate.vue | 2 +- .../project/components/ProjectsTable.vue | 47 +-- .../components/VersionDetailSidebar.vue | 20 +- .../packages/lib/src/modules/project/types.ts | 7 - .../user/components/ChangePasswordForm.vue | 2 +- .../user/components/EditProfileForm.vue | 6 +- .../packages/lib/src/modules/user/types.ts | 2 + .../modules/user/views/ChangePasswordView.vue | 10 - .../modules/user/views/LoginViewTemplate.vue | 6 +- .../user/views/ProfileViewTemplate.vue | 8 +- 31 files changed, 644 insertions(+), 679 deletions(-) create mode 100644 web-app/packages/admin-lib/src/modules/admin/views/AccountDetailView.vue delete mode 100644 web-app/packages/admin-lib/src/modules/admin/views/ProfileView.vue create mode 100644 web-app/packages/admin-lib/src/shims-pinia.d.ts delete mode 100644 web-app/packages/admin-lib/src/shims-tsx.d.ts diff --git a/web-app/packages/admin-app/src/router.ts b/web-app/packages/admin-app/src/router.ts index f8996e7a..d10c55d3 100644 --- a/web-app/packages/admin-app/src/router.ts +++ b/web-app/packages/admin-app/src/router.ts @@ -3,8 +3,8 @@ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial import { - AccountView - // ProfileView, + AccountsView, + AccountDetailView // SettingsView, // ProjectSettingsView, // ProjectsView, @@ -67,7 +67,7 @@ export const createRouter = (pinia: Pinia) => { path: '/accounts', name: 'accounts', components: { - default: AccountView, + default: AccountsView, sidebar: Sidebar, header: AppHeader }, @@ -77,30 +77,18 @@ export const createRouter = (pinia: Pinia) => { routeUtils.isAuthenticatedGuard(to, from, next, userStore) routeUtils.isSuperUser(to, from, next, userStore) } + }, + { + path: '/user/:username', + name: 'account', + components: { + default: AccountDetailView, + sidebar: Sidebar, + header: AppHeader + }, + props: true } // { - // path: '/user/:username', - // name: 'profile', - // component: ProfileView, - // props: true, - // beforeEnter: async (to, from, next) => { - // const adminStore = useAdminStore(pinia) - // adminStore.setUserAdminProfile(null) - // try { - // await adminStore.fetchUserProfileByName({ - // username: to.params.username - // }) - // next() - // } catch (e) { - // next( - // Error( - // errorUtils.getErrorMessage(e, 'Failed to fetch user profile') - // ) - // ) - // } - // } - // }, - // { // path: '/projects', // name: 'projects', // component: ProjectsView, diff --git a/web-app/packages/admin-app/vite.config.ts b/web-app/packages/admin-app/vite.config.ts index 15f916ec..d5a8efb3 100644 --- a/web-app/packages/admin-app/vite.config.ts +++ b/web-app/packages/admin-app/vite.config.ts @@ -53,6 +53,7 @@ export default defineConfig(({ mode }) => ({ sourcemap: mode !== 'production' }, optimizeDeps: { + include: ['vue-router'], exclude: ['vue', '@mergin', 'vue-demi'], esbuildOptions: { define: { diff --git a/web-app/packages/admin-lib/components.d.ts b/web-app/packages/admin-lib/components.d.ts index fe456069..50387356 100644 --- a/web-app/packages/admin-lib/components.d.ts +++ b/web-app/packages/admin-lib/components.d.ts @@ -7,7 +7,13 @@ export {} declare module 'vue' { export interface GlobalComponents { + PAvatar: typeof import('primevue/avatar')['default'] PButton: typeof import('primevue/button')['default'] + PColumn: typeof import('primevue/column')['default'] + PDataTable: typeof import('primevue/datatable')['default'] + PInputSwitch: typeof import('primevue/inputswitch')['default'] + PInputText: typeof import('primevue/inputtext')['default'] + PPassword: typeof import('primevue/password')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] } 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 7d429b79..3d53e80f 100644 --- a/web-app/packages/admin-lib/src/modules/admin/adminApi.ts +++ b/web-app/packages/admin-lib/src/modules/admin/adminApi.ts @@ -31,16 +31,13 @@ export const AdminApi = { return AdminModule.httpService.get(`/app/admin/users`, { params }) }, - async fetchUserProfileByName( + async fetchUserByName( username: string ): Promise> { - return AdminModule.httpService?.get(`/app/admin/user/${username}?random=${Math.random()}`) + return AdminModule.httpService?.get(`/app/admin/user/${username}`) }, - async deleteUser( - username: number, - withRetry?: boolean - ): Promise> { + async deleteUser(username: number): Promise> { return AdminModule.httpService.delete( `/app/admin/user/${username}` /*, { ...(withRetry ? getDefaultRetryOptions() : {}) @@ -50,8 +47,7 @@ export const AdminApi = { async updateUser( username: string, - data: UpdateUserData, - withRetry?: boolean + data: UpdateUserData ): Promise> { return AdminModule.httpService.patch( `/app/admin/user/${username}`, @@ -118,9 +114,12 @@ export const AdminApi = { * @return Result promise */ async removeProject(id: number): Promise> { - return await AdminModule.httpService.delete(`/app/project/removed-project/${id}`, { - 'axios-retry': { retries: 5 } - }) + return await AdminModule.httpService.delete( + `/app/project/removed-project/${id}`, + { + 'axios-retry': { retries: 5 } + } + ) }, /** @@ -129,8 +128,12 @@ export const AdminApi = { * @return Result promise */ async restoreProject(id: number): Promise> { - return await AdminModule.httpService.post(`/app/project/removed-project/restore/${id}`, null, { - 'axios-retry': { retries: 5 } - }) + return await AdminModule.httpService.post( + `/app/project/removed-project/restore/${id}`, + null, + { + 'axios-retry': { retries: 5 } + } + ) } } diff --git a/web-app/packages/admin-lib/src/modules/admin/components/CreateUserForm.vue b/web-app/packages/admin-lib/src/modules/admin/components/CreateUserForm.vue index 08835a40..a97323a5 100644 --- a/web-app/packages/admin-lib/src/modules/admin/components/CreateUserForm.vue +++ b/web-app/packages/admin-lib/src/modules/admin/components/CreateUserForm.vue @@ -5,79 +5,79 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial --> - - - + 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 d85a6873..244e5dd4 100644 --- a/web-app/packages/admin-lib/src/modules/admin/store.ts +++ b/web-app/packages/admin-lib/src/modules/admin/store.ts @@ -14,7 +14,7 @@ import { import { defineStore, getActivePinia } from 'pinia' import Cookies from 'universal-cookie' import { AdminApi } from '@/modules/admin/adminApi' -import { UpdateUserPayload } from '@/modules/admin/types' +import { UpdateUserPayload, UsersResponse } from '@/modules/admin/types' export interface AdminState { loading: boolean @@ -22,7 +22,7 @@ export interface AdminState { items: UserResponse[] count: number } - userAdminProfile?: UserResponse + user?: UserResponse checkForUpdates?: boolean info_url?: string isServerConfigHidden: boolean @@ -38,7 +38,7 @@ export const useAdminStore = defineStore('adminModule', { items: [], count: 0 }, - userAdminProfile: null, + user: null, checkForUpdates: undefined, info_url: undefined, isServerConfigHidden: false @@ -54,12 +54,8 @@ export const useAdminStore = defineStore('adminModule', { setLoading(value) { this.loading = value }, - setUsers(data) { - this.users.count = data.total - this.users.items = data.users - }, - setUserAdminProfile(userAdminProfile) { - this.userAdminProfile = userAdminProfile + setUsers(data: UsersResponse) { + this.users = data }, setCheckForUpdates(value) { this.checkForUpdates = value @@ -84,14 +80,14 @@ export const useAdminStore = defineStore('adminModule', { this.setLoading(false) } }, - async fetchUserProfileByName(payload) { + async fetchUserByName(payload) { const notificationStore = useNotificationStore() htmlUtils.waitCursor(true) try { - const response = await AdminApi.fetchUserProfileByName(payload.username) - this.setUserAdminProfile(response.data) - } catch(e) { + const response = await AdminApi.fetchUserByName(payload.username) + this.user = response.data + } catch (e) { await notificationStore.error({ text: 'Failed to fetch user profile' }) } finally { htmlUtils.waitCursor(false) @@ -123,11 +119,10 @@ export const useAdminStore = defineStore('adminModule', { payload.username, payload.data ) - if (this.userAdminProfile?.id === response.data?.id) { + if (this.user?.id === response.data?.id) { // update stored user detail data - this.setUserAdminProfile(response.data) + this.user = response.data } - await getActivePinia().router.push({ name: 'accounts' }) } catch (err) { await notificationStore.error({ text: errorUtils.getErrorMessage( 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 823af2ad..b5ab01a5 100644 --- a/web-app/packages/admin-lib/src/modules/admin/types.ts +++ b/web-app/packages/admin-lib/src/modules/admin/types.ts @@ -5,6 +5,7 @@ /* eslint-disable camelcase */ import { PaginatedRequestParams, + PaginatedResponse, PaginatedResponseDefaults, Project, ProjectListItem, @@ -15,9 +16,7 @@ export interface UsersParams extends PaginatedRequestParams { username?: string } -export interface UsersResponse extends PaginatedResponseDefaults { - users: UserResponse[] -} +export type UsersResponse = PaginatedResponse export interface UpdateUserData { is_admin: boolean diff --git a/web-app/packages/admin-lib/src/modules/admin/views/AccountDetailView.vue b/web-app/packages/admin-lib/src/modules/admin/views/AccountDetailView.vue new file mode 100644 index 00000000..f13bc773 --- /dev/null +++ b/web-app/packages/admin-lib/src/modules/admin/views/AccountDetailView.vue @@ -0,0 +1,286 @@ + + + + + diff --git a/web-app/packages/admin-lib/src/modules/admin/views/AccountsView.vue b/web-app/packages/admin-lib/src/modules/admin/views/AccountsView.vue index 644493c8..feeff69c 100644 --- a/web-app/packages/admin-lib/src/modules/admin/views/AccountsView.vue +++ b/web-app/packages/admin-lib/src/modules/admin/views/AccountsView.vue @@ -1,9 +1,3 @@ - - - + + + + + + + + @@ -121,11 +101,17 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial import { PaginatedUsersParams, useDialogStore, + AppSection, AppContainer, - AppSection + TableDataHeader } from '@mergin/lib' import debounce from 'lodash/debounce' import { mapActions, mapState } from 'pinia' +import { + DataTablePageEvent, + DataTableRowClickEvent, + DataTableSortEvent +} from 'primevue/datatable' import { defineComponent } from 'vue' import AdminLayout from '@/modules/admin/components/AdminLayout.vue' @@ -144,20 +130,17 @@ export default defineComponent({ options: { sortBy: ['username'], sortDesc: [false], - itemsPerPage: 10, - page: 1 + itemsPerPage: 20, + page: 1, + perPageOptions: [20, 50, 100] }, + searchByName: '', headers: [ - { text: 'Name', value: 'username', filterable: true, sortable: true }, - { - text: 'Active', - value: 'active', - filterable: false, - sortable: false, - width: 15 - } - ], - searchByName: '' + { field: 'username', header: 'Name', sortable: true }, + { field: 'email', header: 'Email', sortable: true }, + { field: 'profile.name', header: 'Username' }, + { field: 'active', header: 'Active' } + ] as TableDataHeader[] } }, computed: { @@ -165,41 +148,66 @@ export default defineComponent({ }, created() { this.resetPaging = debounce(this.resetPaging, 1000) + this.fetchUsers({ params: this.getParams() }) }, methods: { ...mapActions(useAdminStore, ['fetchUsers']), ...mapActions(useDialogStore, ['show']), - async resetPaging() { + onSearch() { this.options.page = 1 - await this.paginate(this.options) + this.fetchUsers({ params: this.getParams() }) }, - async resetSearch() { - this.searchByName = '' - await this.resetPaging() + async resetPaging() { + this.options.page = 1 }, - async paginate(options) { - this.options = options + getParams(): PaginatedUsersParams { const params = { - page: options.page, - per_page: options.itemsPerPage + page: this.options.page, + per_page: this.options.itemsPerPage } as PaginatedUsersParams - if (options.sortBy[0]) { - params.descending = options.sortDesc[0] - params.order_by = options.sortBy[0] + if (this.options.sortBy[0]) { + params.descending = this.options.sortDesc[0] + params.order_by = this.options.sortBy[0] } if (this.searchByName) { params.like = this.searchByName.trim() } - await this.fetchUsers({ params }) + return params + }, + + refresh() { + this.fetchUsers({ params: this.getParams() }) + }, + + onPage(event: DataTablePageEvent) { + this.options.page = event.page + 1 + this.options.itemsPerPage = event.rows + this.fetchUsers({ params: this.getParams() }) + }, + + onSort(event: DataTableSortEvent) { + this.options.sortBy[0] = event.sortField?.toString() + this.options.sortDesc[0] = event.sortOrder < 1 + this.fetchUsers({ params: this.getParams() }) + }, + + rowClick(event: DataTableRowClickEvent) { + this.$router.push({ + name: 'account', + params: { username: event.data.username } + }) }, createUserDialog() { - const dialog = { maxWidth: 500, persistent: true } + const dialog = { maxWidth: 500, header: 'Create user' } const listeners = { - success: async () => this.resetPaging() + success: () => { + this.resetPaging() + this.fetchUsers({ params: this.getParams() }) + } } this.show({ component: CreateUserForm, @@ -212,10 +220,3 @@ export default defineComponent({ } }) - - diff --git a/web-app/packages/admin-lib/src/modules/admin/views/ProfileView.vue b/web-app/packages/admin-lib/src/modules/admin/views/ProfileView.vue deleted file mode 100644 index ba34a195..00000000 --- a/web-app/packages/admin-lib/src/modules/admin/views/ProfileView.vue +++ /dev/null @@ -1,290 +0,0 @@ - - - - - - - diff --git a/web-app/packages/admin-lib/src/modules/admin/views/index.ts b/web-app/packages/admin-lib/src/modules/admin/views/index.ts index 47a6fcd0..6d414de0 100644 --- a/web-app/packages/admin-lib/src/modules/admin/views/index.ts +++ b/web-app/packages/admin-lib/src/modules/admin/views/index.ts @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial -export { default as AccountView } from './AccountsView.vue' -export { default as ProfileView } from './ProfileView.vue' +export { default as AccountsView } from './AccountsView.vue' +export { default as AccountDetailView } from './AccountDetailView.vue' export { default as SettingsView } from './SettingsView.vue' export { default as SettingsViewTemplate } from './SettingsViewTemplate.vue' diff --git a/web-app/packages/admin-lib/src/shims-pinia.d.ts b/web-app/packages/admin-lib/src/shims-pinia.d.ts new file mode 100644 index 00000000..4a4f5ef4 --- /dev/null +++ b/web-app/packages/admin-lib/src/shims-pinia.d.ts @@ -0,0 +1,7 @@ +import { Router } from 'vue-router' + +declare module 'pinia' { + export interface Pinia { + router: Router + } +} diff --git a/web-app/packages/admin-lib/src/shims-tsx.d.ts b/web-app/packages/admin-lib/src/shims-tsx.d.ts deleted file mode 100644 index e73203a2..00000000 --- a/web-app/packages/admin-lib/src/shims-tsx.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (C) Lutra Consulting Limited -// -// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial - -import Vue, { VNode } from 'vue' - -declare global { - namespace JSX { - interface Element extends VNode {} - interface ElementClass extends Vue {} - interface IntrinsicElements { - [elem: string]: any - } - } -} diff --git a/web-app/packages/admin-lib/src/shims-vue.d.ts b/web-app/packages/admin-lib/src/shims-vue.d.ts index 42ada501..0e8a3633 100644 --- a/web-app/packages/admin-lib/src/shims-vue.d.ts +++ b/web-app/packages/admin-lib/src/shims-vue.d.ts @@ -2,15 +2,20 @@ // // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial -import Router from 'vue-router' +import { MerginComponentUuid } from './modules/form/types' + +import { ComponentCustomPropertyFilters } from '@/common' declare module '*.vue' { - import Vue from 'vue' + import * as Vue from 'vue' export default Vue } -declare module 'pinia' { - export interface Pinia { - router: Router +// It seems that IntelliJ IDEs are using the @vue/runtime-core module for type checking in .vue