Skip to content
Open
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
39 changes: 3 additions & 36 deletions webapp/packages/core-authentication/src/UsersResource.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -35,14 +35,12 @@ import { Executor } from '@cloudbeaver/core-executor';
import { AUTH_PROVIDER_LOCAL_ID } from './AUTH_PROVIDER_LOCAL_ID.js';
import { AuthInfoService } from './AuthInfoService.js';
import { AuthProviderService } from './AuthProviderService.js';
import { compareUsersById, isNewUser, NEW_USER_SYMBOL, type AdminUserNew } from './compareUser.js';
import type { IAuthCredentials } from './IAuthCredentials.js';

const NEW_USER_SYMBOL = Symbol('new-user');

export type AdminUser = AdminUserInfoFragment;
export type AdminUserOrigin = AdminUserInfoFragment['origins'][number];

type AdminUserNew = AdminUser & { [NEW_USER_SYMBOL]: boolean; createdAt: number };
export type UserResourceIncludes = Omit<GetUsersListQueryVariables, 'userId' | 'page' | 'filter'>;

interface IUserResourceFilterOptions {
Expand Down Expand Up @@ -96,7 +94,7 @@ export class UsersResource extends CachedMapResource<string, AdminUser, UserReso
this.aliases.add(UsersResourceNewUsers, () => {
const orderedKeys = this.entries
.filter(k => isNewUser(k[1]))
.sort((a, b) => compareUsers(a[1], b[1]))
.sort((a, b) => compareUsersById(a[1], b[1]))
.map(([key]) => key);
return resourceKeyList(orderedKeys);
});
Expand Down Expand Up @@ -328,34 +326,3 @@ export class UsersResource extends CachedMapResource<string, AdminUser, UserReso
return typeof key === 'string';
}
}

export function isLocalUser(user: AdminUser): boolean {
return user.origins.some(origin => origin.type === AUTH_PROVIDER_LOCAL_ID);
}

export function isNewUser(user: AdminUser | AdminUserNew): user is AdminUserNew {
return NEW_USER_SYMBOL in user && user[NEW_USER_SYMBOL] === true && 'createdAt' in user && Boolean(user.createdAt);
}

export function compareUsers<T extends Pick<AdminUser, 'userId'>>(a: T, b: T): number {
return a.userId.localeCompare(b.userId);
}

export function compareNewUsers(a: AdminUser, b: AdminUser): number {
const aIsNew = isNewUser(a);
const bIsNew = isNewUser(b);

if (aIsNew && !bIsNew) {
return -1;
}

if (!aIsNew && bIsNew) {
return 1;
}

if (aIsNew && bIsNew) {
return b.createdAt - a.createdAt;
}

return 0;
}
23 changes: 23 additions & 0 deletions webapp/packages/core-authentication/src/compareGrantSubjects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { compareUsersByLastLogin, isUser } from './compareUser.js';
import type { TeamInfo } from './TeamsResource.js';
import type { AdminUser } from './UsersResource.js';

export function compareGrantSubjectsByName(a: AdminUser | TeamInfo, b: AdminUser | TeamInfo): number {
const aName = isUser(a) ? a.userId : a.teamName;
const bName = isUser(b) ? b.userId : b.teamName;
return (aName ?? '').localeCompare(bName ?? '');
}

export function compareGrantSubjectsByLastLogin(a: AdminUser | TeamInfo, b: AdminUser | TeamInfo): number {
return compareUsersByLastLogin(
{ lastLoginTime: isUser(a) ? a.lastLoginTime : undefined },
{ lastLoginTime: isUser(b) ? b.lastLoginTime : undefined },
);
}
54 changes: 54 additions & 0 deletions webapp/packages/core-authentication/src/compareUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { AUTH_PROVIDER_LOCAL_ID } from './AUTH_PROVIDER_LOCAL_ID.js';
import type { AdminUser } from './UsersResource.js';

export const NEW_USER_SYMBOL = Symbol('new-user');

export type AdminUserNew = AdminUser & { [NEW_USER_SYMBOL]: boolean; createdAt: number };

export function isLocalUser(user: AdminUser): boolean {
return user.origins.some(origin => origin.type === AUTH_PROVIDER_LOCAL_ID);
}

export function isNewUser(user: AdminUser | AdminUserNew): user is AdminUserNew {
return NEW_USER_SYMBOL in user && user[NEW_USER_SYMBOL] === true && 'createdAt' in user && Boolean(user.createdAt);
}

export function isUser(user: unknown): user is AdminUser {
return !!user && typeof user === 'object' && 'userId' in user && 'grantedTeams' in user && 'enabled' in user;
}

export function compareUsersById<T extends Pick<AdminUser, 'userId'>>(a: T, b: T): number {
return a.userId.localeCompare(b.userId);
}

export function compareUsersByLastLogin<T extends Pick<AdminUser, 'lastLoginTime'>>(a: T, b: T): number {
const aTime = a.lastLoginTime ? new Date(a.lastLoginTime).getTime() : 0;
const bTime = b.lastLoginTime ? new Date(b.lastLoginTime).getTime() : 0;
return aTime - bTime;
}

export function compareUsersByNewness<T extends AdminUser>(a: T, b: T): number {
const aIsNew = isNewUser(a);
const bIsNew = isNewUser(b);

if (aIsNew && !bIsNew) {
return -1;
}

if (!aIsNew && bIsNew) {
return 1;
}

if (aIsNew && bIsNew) {
return b.createdAt - a.createdAt;
}

return 0;
}
2 changes: 2 additions & 0 deletions webapp/packages/core-authentication/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ export * from './UserInfoResource.js';
export * from './UserMetaParametersResource.js';
export * from './UsersMetaParametersResource.js';
export * from './UsersResource.js';
export * from './compareUser.js';
export * from './UsersOriginDetailsResource.js';
export * from './UserInfoMetaParametersResource.js';
export * from './TeamMetaParametersResource.js';
export * from './compareGrantSubjects.js';
export * from './AUTH_SETTINGS_GROUP.js';
export * from './PasswordPolicyService.js';
export * from './TeamRolesResource.js';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import { CachedResourceOffsetPageListKey } from '@cloudbeaver/core-resource';
import { GrantManagementTable, type IGrantManagementTableColumn } from '@cloudbeaver/plugin-data-grid';
import { ServerConfigResource } from '@cloudbeaver/core-root';
import {
compareUsers,
compareUsersById,
compareUsersByLastLogin,
TeamRolesResource,
USER_TEAM_ROLE_SUPERVISOR,
UsersResource,
Expand All @@ -24,15 +25,21 @@ import {

import type { TeamFormProps } from '../TeamsAdministrationFormService.js';
import type { GrantedUsersFormPart } from './GrantedUsersFormPart.js';
import { useMemo } from 'react';

const USER_ID_COLUMN: IGrantManagementTableColumn = { key: 'userId', label: 'administration_teams_team_granted_users_user_id' };
const LAST_LOGIN_COLUMN: IGrantManagementTableColumn = { key: 'lastLogin', label: 'plugin_authentication_administration_user_last_login' };
const TEAM_ROLE_COLUMN: IGrantManagementTableColumn = {
key: 'teamRole',
label: 'plugin_authentication_administration_team_user_team_role_supervisor',
const USER_ID_COLUMN: IGrantManagementTableColumn<AdminUser> = {
key: 'userId',
label: 'administration_teams_team_granted_users_user_id',
compare: compareUsersById,
};
const LAST_LOGIN_COLUMN: IGrantManagementTableColumn<AdminUser> = {
key: 'lastLogin',
label: 'plugin_authentication_administration_user_last_login',
compare: compareUsersByLastLogin,
};
const TEAM_ROLE_COLUMN_KEY = 'teamRole';

const COLUMNS: IGrantManagementTableColumn[] = [USER_ID_COLUMN, LAST_LOGIN_COLUMN];
const COLUMNS: IGrantManagementTableColumn<AdminUser>[] = [USER_ID_COLUMN, LAST_LOGIN_COLUMN];

export const GrantedUsersTable: TabContainerPanelComponent<TeamFormProps> = observer(function GrantedUsersTable({ tabId, formState }) {
const translate = useTranslate();
Expand All @@ -48,6 +55,7 @@ export const GrantedUsersTable: TabContainerPanelComponent<TeamFormProps> = obse
const usersLoader = useResource(GrantedUsersTable, UsersResource, CachedResourceOffsetPageListKey(0, 1000).setParent(UsersResourceFilterKey()), {
active,
});
const grantedUsersIdsMap = useMemo(() => new Map(tabState.state.grantedUsers.map(user => [user.userId, user])), [tabState.state.grantedUsers]);

useAutoLoad(GrantedUsersTable, tabState, active);

Expand Down Expand Up @@ -78,10 +86,24 @@ export const GrantedUsersTable: TabContainerPanelComponent<TeamFormProps> = obse
return !usersLoader.resource.isActiveUser(user.userId);
}

function getTeamRoleRank(user: AdminUser) {
const granted = grantedUsersIdsMap.get(user.userId);

if (!granted) {
return 0;
}

return granted.teamRole === USER_TEAM_ROLE_SUPERVISOR ? 2 : 1;
}

const columns = [...COLUMNS];

if (teamRolesResource.data.length > 0) {
columns.push(TEAM_ROLE_COLUMN);
columns.push({
key: TEAM_ROLE_COLUMN_KEY,
label: 'plugin_authentication_administration_team_user_team_role_supervisor',
compare: (a, b) => getTeamRoleRank(a) - getTeamRoleRank(b),
});
Comment on lines +89 to +106

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so for every single compare we do 2 x O(n) searches among grantedUsers.
It might be a problem on big lists.
Let's optimize it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in data structure & algorithms world 2 * O(N) = O(N) and its fine. computers and browsers can handle that easy peasy
I think this matters only if you develop search engine or computer chip architecture or something like that

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but how the compare function is used?

it's basically O(n) * compare (O(n))

}

function getCell(user: AdminUser, colKey: string) {
Expand Down Expand Up @@ -114,7 +136,7 @@ export const GrantedUsersTable: TabContainerPanelComponent<TeamFormProps> = obse
return <span title={lastLoginFullTime}>{lastLoginDate}</span>;
}

if (colKey === TEAM_ROLE_COLUMN.key) {
if (colKey === TEAM_ROLE_COLUMN_KEY) {
const granted = tabState.state.grantedUsers.find(grantedUser => grantedUser.userId === user.userId);

if (granted) {
Expand All @@ -134,7 +156,7 @@ export const GrantedUsersTable: TabContainerPanelComponent<TeamFormProps> = obse
return null;
}

const items = (usersLoader.data.filter(user => user?.enabled) as AdminUser[]).sort(compareUsers);
const items = (usersLoader.data.filter(user => user?.enabled) as AdminUser[]).sort(compareUsersById);

return (
<GrantManagementTable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
import { action, computed, observable } from 'mobx';

import { type AdminUser, compareUsers, compareNewUsers, UsersResource, UsersResourceFilterKey } from '@cloudbeaver/core-authentication';
import { type AdminUser, compareUsersById, compareUsersByNewness, UsersResource, UsersResourceFilterKey } from '@cloudbeaver/core-authentication';
import { TableState, useExecutor, useObservableRef, useOffsetPagination, useResource } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
Expand All @@ -24,9 +24,9 @@
update: () => void;
}

export function useUsersTable(filters: IUserFilters) {

Check warning on line 27 in webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/useUsersTable.tsx

View workflow job for this annotation

GitHub Actions / Frontend / Lint

Missing return type on function
const searchFilter = filters.search.trim().toLowerCase();
const enabledStateFilter = filters.status === 'true' ? true : filters.status === 'false' ? false : undefined;

Check warning on line 29 in webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/useUsersTable.tsx

View workflow job for this annotation

GitHub Actions / Frontend / Lint

Do not nest ternary expressions
const pagination = useOffsetPagination(UsersResource, {
key: UsersResourceFilterKey(searchFilter, enabledStateFilter),
});
Expand All @@ -35,14 +35,14 @@
const notificationService = useService(NotificationService);

useExecutor({
executor: usersLoader.resource.onUserCreate, handlers: [
executor: usersLoader.resource.onUserCreate,
handlers: [
function handleUserCreation() {
pagination.refresh();
},
],
});


const state: State = useObservableRef(
() => ({
loading: false,
Expand All @@ -51,7 +51,7 @@
return pagination.hasNextPage;
},
get users() {
usersLoader.data; // triggers suspense, behaves differently than forceSuspense action cause used on the getter level

Check warning on line 54 in webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/useUsersTable.tsx

View workflow job for this annotation

GitHub Actions / Frontend / Lint

Expected an assignment or function call and instead saw an expression

return filters.filterUsers(
Array.from(
Expand All @@ -61,8 +61,8 @@
]),
)
.filter(isDefined)
.sort(compareUsers)
.sort(compareNewUsers),
.sort(compareUsersById)
.sort(compareUsersByNewness),
);
},
update() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,33 @@ import { Alert, StaticImage, useAutoLoad, useResource, useTranslate } from '@clo
import { useTab, type TabContainerPanelComponent } from '@cloudbeaver/core-ui';
import { getConnectionFormOptionsPart, type IConnectionFormProps } from '@cloudbeaver/plugin-connections';
import { CachedMapAllKey, CachedResourceOffsetPageListKey } from '@cloudbeaver/core-resource';
import { TeamsResource, UsersResource, UsersResourceFilterKey, type AdminUser, type TeamInfo } from '@cloudbeaver/core-authentication';
import {
compareGrantSubjectsByLastLogin,
compareGrantSubjectsByName,
TeamsResource,
UsersResource,
UsersResourceFilterKey,
type AdminUser,
type TeamInfo,
} from '@cloudbeaver/core-authentication';
import { ConnectionInfoOriginResource, ConnectionInfoResource, createConnectionParam, isCloudConnection } from '@cloudbeaver/core-connections';
import { GrantManagementTable, type IGrantManagementTableColumn } from '@cloudbeaver/plugin-data-grid';

import { getConnectionFormAccessPart } from './getConnectionFormAccessPart.js';

const NAME_COLUMN: IGrantManagementTableColumn = { key: 'name', label: 'connections_connection_access_user_or_team_name' };
const DESCRIPTION_COLUMN: IGrantManagementTableColumn = { key: 'description', label: 'connections_connection_description' };
const LAST_LOGIN_COLUMN: IGrantManagementTableColumn = { key: 'lastLogin', label: 'plugin_connections_administration_user_last_login' };

const COLUMNS: IGrantManagementTableColumn[] = [NAME_COLUMN, DESCRIPTION_COLUMN, LAST_LOGIN_COLUMN];
const NAME_COLUMN: IGrantManagementTableColumn<AdminUser | TeamInfo> = {
key: 'name',
label: 'connections_connection_access_user_or_team_name',
compare: compareGrantSubjectsByName,
};
const DESCRIPTION_COLUMN: IGrantManagementTableColumn<AdminUser | TeamInfo> = { key: 'description', label: 'connections_connection_description' };
const LAST_LOGIN_COLUMN: IGrantManagementTableColumn<AdminUser | TeamInfo> = {
key: 'lastLogin',
label: 'plugin_connections_administration_user_last_login',
compare: compareGrantSubjectsByLastLogin,
};

const COLUMNS: IGrantManagementTableColumn<AdminUser | TeamInfo>[] = [NAME_COLUMN, DESCRIPTION_COLUMN, LAST_LOGIN_COLUMN];

export const ConnectionAccessTable: TabContainerPanelComponent<IConnectionFormProps> = observer(function ConnectionAccessTable({ tabId, formState }) {
const translate = useTranslate();
Expand Down
Loading
Loading