mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
CB-3564 multi server license (#2062)
* CB-3564 fix: localization * CB-3984 wip * CB-3984 multi instance license * CB-3564 feat: external users provider * CB-3984 server init fix * CB-3564 feat: load resource for administration users management * CB-3984 force enable imported user --------- Co-authored-by: Aleksandr Skoblikov <aleksandr.skoblikov@dbeaver.com> Co-authored-by: EvgeniaBzzz <139753579+EvgeniaBzzz@users.noreply.github.com>
This commit is contained in:
co-authored by
Aleksandr Skoblikov
EvgeniaBzzz
parent
d87115d753
commit
d44e6b4e7a
Vendored
+7
-1
@@ -10,7 +10,13 @@
|
||||
"sourceMaps": true,
|
||||
"sourceMapPathOverrides": {
|
||||
"webpack:///*": "${workspaceFolder}/../*"
|
||||
}
|
||||
},
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**",
|
||||
"${workspaceFolder}/webapp/**/node_modules/**/*.js",
|
||||
"${workspaceFolder}/webapp/**/dist/**/*.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "java",
|
||||
|
||||
@@ -325,14 +325,6 @@ public abstract class CBApplication extends BaseWebApplication implements WebAut
|
||||
|
||||
}
|
||||
|
||||
{
|
||||
try {
|
||||
initializeSecurityController();
|
||||
} catch (Exception e) {
|
||||
log.error("Error initializing database", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
initializeServer();
|
||||
} catch (DBException e) {
|
||||
@@ -340,6 +332,14 @@ public abstract class CBApplication extends BaseWebApplication implements WebAut
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
initializeSecurityController();
|
||||
} catch (Exception e) {
|
||||
log.error("Error initializing database", e);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (configurationMode) {
|
||||
// Try to configure automatically
|
||||
performAutoConfiguration(configPath.toFile().getParentFile());
|
||||
|
||||
+97
-55
@@ -67,7 +67,8 @@ import java.util.stream.Collectors;
|
||||
/**
|
||||
* Server controller
|
||||
*/
|
||||
public class CBEmbeddedSecurityController implements SMAdminController, SMAuthenticationManager {
|
||||
public class CBEmbeddedSecurityController<T extends WebAuthApplication>
|
||||
implements SMAdminController, SMAuthenticationManager {
|
||||
|
||||
private static final Log log = Log.getLog(CBEmbeddedSecurityController.class);
|
||||
|
||||
@@ -80,14 +81,14 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
|
||||
}.getType();
|
||||
private static final Gson gson = new GsonBuilder().create();
|
||||
|
||||
protected final WebAuthApplication application;
|
||||
protected final T application;
|
||||
protected final CBDatabase database;
|
||||
protected final SMCredentialsProvider credentialsProvider;
|
||||
|
||||
protected final SMControllerConfiguration smConfig;
|
||||
|
||||
public CBEmbeddedSecurityController(
|
||||
WebAuthApplication application,
|
||||
T application,
|
||||
CBDatabase database,
|
||||
SMCredentialsProvider credentialsProvider,
|
||||
SMControllerConfiguration smConfig
|
||||
@@ -132,44 +133,69 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
|
||||
log.debug("Create user: " + userId);
|
||||
try (Connection dbCon = database.openConnection()) {
|
||||
try (JDBCTransaction txn = new JDBCTransaction(dbCon)) {
|
||||
createAuthSubject(dbCon, userId, SUBJECT_USER);
|
||||
try (PreparedStatement dbStat = dbCon.prepareStatement(
|
||||
database.normalizeTableNames("INSERT INTO {table_prefix}CB_USER" +
|
||||
"(USER_ID,IS_ACTIVE,CREATE_TIME,DEFAULT_AUTH_ROLE) VALUES(?,?,?,?)"))
|
||||
) {
|
||||
dbStat.setString(1, userId);
|
||||
dbStat.setString(2, enabled ? CHAR_BOOL_TRUE : CHAR_BOOL_FALSE);
|
||||
dbStat.setTimestamp(3, new Timestamp(System.currentTimeMillis()));
|
||||
if (CommonUtils.isEmpty(defaultAuthRole)) {
|
||||
dbStat.setNull(4, Types.VARCHAR);
|
||||
} else {
|
||||
dbStat.setString(4, defaultAuthRole);
|
||||
}
|
||||
dbStat.execute();
|
||||
createUser(dbCon, userId, metaParameters, enabled, defaultAuthRole);
|
||||
String defaultTeamName = application.getAppConfiguration().getDefaultUserTeam();
|
||||
if (!CommonUtils.isEmpty(defaultTeamName)) {
|
||||
setUserTeams(dbCon, userId, new String[]{defaultTeamName}, userId);
|
||||
}
|
||||
saveSubjectMetas(dbCon, userId, metaParameters);
|
||||
txn.commit();
|
||||
}
|
||||
String defaultTeamName = application.getAppConfiguration().getDefaultUserTeam();
|
||||
if (!CommonUtils.isEmpty(defaultTeamName)) {
|
||||
setUserTeams(userId, new String[]{defaultTeamName}, userId);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new DBCException("Error saving user in database", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void createUser(
|
||||
@NotNull Connection dbCon,
|
||||
@NotNull String userId,
|
||||
@Nullable Map<String, String> metaParameters,
|
||||
boolean enabled,
|
||||
@Nullable String defaultAuthRole
|
||||
) throws DBException, SQLException {
|
||||
createAuthSubject(dbCon, userId, SUBJECT_USER);
|
||||
try (PreparedStatement dbStat = dbCon.prepareStatement(
|
||||
database.normalizeTableNames("INSERT INTO {table_prefix}CB_USER" +
|
||||
"(USER_ID,IS_ACTIVE,CREATE_TIME,DEFAULT_AUTH_ROLE) VALUES(?,?,?,?)"))
|
||||
) {
|
||||
dbStat.setString(1, userId);
|
||||
dbStat.setString(2, enabled ? CHAR_BOOL_TRUE : CHAR_BOOL_FALSE);
|
||||
dbStat.setTimestamp(3, new Timestamp(System.currentTimeMillis()));
|
||||
if (CommonUtils.isEmpty(defaultAuthRole)) {
|
||||
dbStat.setNull(4, Types.VARCHAR);
|
||||
} else {
|
||||
dbStat.setString(4, defaultAuthRole);
|
||||
}
|
||||
dbStat.execute();
|
||||
}
|
||||
saveSubjectMetas(dbCon, userId, metaParameters);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importUsers(@NotNull SMUserImportList userImportList) throws DBException {
|
||||
for (SMUserProvisioning user : userImportList.getUsers()) {
|
||||
if (isSubjectExists(user.getUserId())) {
|
||||
log.info("Skip already exist user: " + user.getUserId());
|
||||
setUserAuthRole(user.getUserId(), userImportList.getAuthRole());
|
||||
continue;
|
||||
}
|
||||
createUser(user.getUserId(), user.getMetaParameters(), true, userImportList.getAuthRole());
|
||||
}
|
||||
}
|
||||
|
||||
protected void importUsers(@NotNull Connection connection, @NotNull SMUserImportList userImportList)
|
||||
throws DBException, SQLException {
|
||||
for (SMUserProvisioning user : userImportList.getUsers()) {
|
||||
if (isSubjectExists(user.getUserId())) {
|
||||
log.info("User already exist : " + user.getUserId());
|
||||
setUserAuthRole(connection, user.getUserId(), userImportList.getAuthRole());
|
||||
enableUser(connection, user.getUserId(), true);
|
||||
continue;
|
||||
}
|
||||
createUser(connection, user.getUserId(), user.getMetaParameters(), true, userImportList.getAuthRole());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteUser(String userId) throws DBCException {
|
||||
invalidateAllUserTokens(userId);
|
||||
@@ -192,25 +218,7 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
|
||||
public void setUserTeams(String userId, String[] teamIds, String grantorId) throws DBCException {
|
||||
try (Connection dbCon = database.openConnection()) {
|
||||
try (JDBCTransaction txn = new JDBCTransaction(dbCon)) {
|
||||
JDBCUtils.executeStatement(
|
||||
dbCon,
|
||||
database.normalizeTableNames("DELETE FROM {table_prefix}CB_USER_TEAM WHERE USER_ID=?"),
|
||||
userId
|
||||
);
|
||||
if (!ArrayUtils.isEmpty(teamIds)) {
|
||||
try (PreparedStatement dbStat = dbCon.prepareStatement(
|
||||
database.normalizeTableNames("INSERT INTO {table_prefix}CB_USER_TEAM" +
|
||||
"(USER_ID,TEAM_ID,GRANT_TIME,GRANTED_BY) VALUES(?,?,?,?)"))
|
||||
) {
|
||||
for (String teamId : teamIds) {
|
||||
dbStat.setString(1, userId);
|
||||
dbStat.setString(2, teamId);
|
||||
dbStat.setTimestamp(3, new Timestamp(System.currentTimeMillis()));
|
||||
dbStat.setString(4, grantorId);
|
||||
dbStat.execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
setUserTeams(dbCon, userId, teamIds, grantorId);
|
||||
txn.commit();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
@@ -219,6 +227,28 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
|
||||
addSubjectPermissionsUpdateEvent(userId, SMSubjectType.user);
|
||||
}
|
||||
|
||||
public void setUserTeams(@NotNull Connection dbCon, String userId, String[] teamIds, String grantorId)
|
||||
throws SQLException {
|
||||
JDBCUtils.executeStatement(
|
||||
dbCon,
|
||||
database.normalizeTableNames("DELETE FROM {table_prefix}CB_USER_TEAM WHERE USER_ID=?"),
|
||||
userId
|
||||
);
|
||||
if (!ArrayUtils.isEmpty(teamIds)) {
|
||||
try (PreparedStatement dbStat = dbCon.prepareStatement(
|
||||
database.normalizeTableNames("INSERT INTO {table_prefix}CB_USER_TEAM" +
|
||||
"(USER_ID,TEAM_ID,GRANT_TIME,GRANTED_BY) VALUES(?,?,?,?)"))
|
||||
) {
|
||||
for (String teamId : teamIds) {
|
||||
dbStat.setString(1, userId);
|
||||
dbStat.setString(2, teamId);
|
||||
dbStat.setTimestamp(3, new Timestamp(System.currentTimeMillis()));
|
||||
dbStat.setString(4, grantorId);
|
||||
dbStat.execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@NotNull
|
||||
@@ -563,17 +593,21 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
|
||||
|
||||
public void enableUser(String userId, boolean enabled) throws DBException {
|
||||
try (Connection dbCon = database.openConnection()) {
|
||||
try (PreparedStatement dbStat = dbCon.prepareStatement(
|
||||
database.normalizeTableNames("UPDATE {table_prefix}CB_USER SET IS_ACTIVE=? WHERE USER_ID=?"))) {
|
||||
dbStat.setString(1, enabled ? CHAR_BOOL_TRUE : CHAR_BOOL_FALSE);
|
||||
dbStat.setString(2, userId);
|
||||
dbStat.executeUpdate();
|
||||
}
|
||||
enableUser(dbCon, userId, enabled);
|
||||
} catch (SQLException e) {
|
||||
throw new DBCException("Error while updating user configuration", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void enableUser(Connection dbCon, String userId, boolean enabled) throws SQLException {
|
||||
try (PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames(
|
||||
"UPDATE {table_prefix}CB_USER SET IS_ACTIVE=? WHERE USER_ID=?"))) {
|
||||
dbStat.setString(1, enabled ? CHAR_BOOL_TRUE : CHAR_BOOL_FALSE);
|
||||
dbStat.setString(2, userId);
|
||||
dbStat.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserAuthRole(@NotNull String userId, @Nullable String authRole) throws DBException {
|
||||
if (credentialsProvider.getActiveUserCredentials() != null
|
||||
@@ -582,20 +616,28 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen
|
||||
throw new SMException("User cannot change his own role");
|
||||
}
|
||||
try (Connection dbCon = database.openConnection()) {
|
||||
try (PreparedStatement dbStat = dbCon.prepareStatement(
|
||||
database.normalizeTableNames("UPDATE {table_prefix}CB_USER SET DEFAULT_AUTH_ROLE=? WHERE USER_ID=?"))) {
|
||||
dbStat.setString(1, authRole);
|
||||
dbStat.setString(2, userId);
|
||||
if (dbStat.executeUpdate() <= 0) {
|
||||
throw new SMException("User not found");
|
||||
}
|
||||
}
|
||||
setUserAuthRole(dbCon, userId, authRole);
|
||||
} catch (SQLException e) {
|
||||
throw new DBCException("Error while updating user authentication role", e);
|
||||
}
|
||||
addSubjectPermissionsUpdateEvent(userId, SMSubjectType.user);
|
||||
}
|
||||
|
||||
public void setUserAuthRole(@NotNull Connection dbCon, @NotNull String userId, @Nullable String authRole)
|
||||
throws DBException, SQLException {
|
||||
try (PreparedStatement dbStat = dbCon.prepareStatement(
|
||||
database.normalizeTableNames("UPDATE {table_prefix}CB_USER SET DEFAULT_AUTH_ROLE=? WHERE USER_ID=?"))) {
|
||||
if (authRole == null) {
|
||||
dbStat.setNull(1, Types.VARCHAR);
|
||||
} else {
|
||||
dbStat.setString(1, authRole);
|
||||
}
|
||||
dbStat.setString(2, userId);
|
||||
if (dbStat.executeUpdate() <= 0) {
|
||||
throw new SMException("User not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////
|
||||
|
||||
+3
-3
@@ -31,7 +31,7 @@ import java.util.Map;
|
||||
/**
|
||||
* Embedded Security Controller Factory
|
||||
*/
|
||||
public class EmbeddedSecurityControllerFactory {
|
||||
public class EmbeddedSecurityControllerFactory<T extends WebAuthApplication> {
|
||||
private static volatile CBDatabase DB_INSTANCE;
|
||||
|
||||
public static CBDatabase getDbInstance() {
|
||||
@@ -42,7 +42,7 @@ public class EmbeddedSecurityControllerFactory {
|
||||
* Create new security controller instance with custom configuration
|
||||
*/
|
||||
public CBEmbeddedSecurityController createSecurityService(
|
||||
WebAuthApplication application,
|
||||
T application,
|
||||
Map<String, Object> databaseConfig,
|
||||
SMCredentialsProvider credentialsProvider,
|
||||
SMControllerConfiguration smConfig
|
||||
@@ -86,7 +86,7 @@ public class EmbeddedSecurityControllerFactory {
|
||||
}
|
||||
|
||||
protected CBEmbeddedSecurityController createEmbeddedSecurityController(
|
||||
WebAuthApplication application,
|
||||
T application,
|
||||
CBDatabase database,
|
||||
SMCredentialsProvider credentialsProvider,
|
||||
SMControllerConfiguration smConfig
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import type { HTMLAttributes, PropsWithChildren } from 'react';
|
||||
|
||||
import { getLayoutProps } from '../Containers/filterLayoutFakeProps';
|
||||
import { filterLayoutFakeProps, getLayoutProps } from '../Containers/filterLayoutFakeProps';
|
||||
import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps';
|
||||
import elementsSizeStyles from '../Containers/shared/ElementsSize.m.css';
|
||||
import { s } from '../s';
|
||||
@@ -14,8 +14,8 @@ type Props = ILayoutSizeProps &
|
||||
};
|
||||
export const Field: React.FC<PropsWithChildren<Props>> = observer(function Field({ children, className, ...rest }) {
|
||||
const styles = useS(fieldStyles, elementsSizeStyles);
|
||||
|
||||
const layoutProps = getLayoutProps(rest);
|
||||
rest = filterLayoutFakeProps(rest);
|
||||
|
||||
return (
|
||||
<div {...rest} className={s(styles, { ...layoutProps, field: true }, className)}>
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
*/
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { isDefined } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { useAutoLoad } from '../Loader/useAutoLoad';
|
||||
import type { PlaceholderContainer, PlaceholderElement } from './PlaceholderContainer';
|
||||
|
||||
type Props<T extends Record<string, any>> = T & {
|
||||
@@ -27,6 +30,14 @@ export const Placeholder = observer(function Placeholder<T extends Record<string
|
||||
});
|
||||
}
|
||||
|
||||
useAutoLoad(
|
||||
Placeholder,
|
||||
elements
|
||||
.map(element => element.getLoaders?.(rest as unknown as T))
|
||||
.flat()
|
||||
.filter(isDefined),
|
||||
);
|
||||
|
||||
elements = elements.filter(placeholder => !placeholder.isHidden?.(rest as unknown as T));
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
import { observable } from 'mobx';
|
||||
|
||||
import { uuid } from '@cloudbeaver/core-utils';
|
||||
import { ILoadableState, uuid } from '@cloudbeaver/core-utils';
|
||||
|
||||
export type PlaceholderComponent<T extends Record<string, any> = Record<string, any>> = React.FunctionComponent<T>;
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface PlaceholderElement<T extends Record<string, any> = Record<strin
|
||||
component: PlaceholderComponent<T>;
|
||||
isHidden?: (props: T) => boolean;
|
||||
order?: number;
|
||||
getLoaders?: (props: T) => ILoadableState[];
|
||||
}
|
||||
|
||||
export class PlaceholderContainer<T extends Record<string, any> = Record<string, any>> {
|
||||
@@ -29,12 +30,13 @@ export class PlaceholderContainer<T extends Record<string, any> = Record<string,
|
||||
return this.placeholders.filter(placeholder => !placeholder.isHidden?.(props));
|
||||
}
|
||||
|
||||
add(component: PlaceholderComponent<T>, order?: number, isHidden?: (props: T) => boolean): void {
|
||||
add(component: PlaceholderComponent<T>, order?: number, isHidden?: (props: T) => boolean, getLoaders?: (props: T) => ILoadableState[]): void {
|
||||
const placeholder: PlaceholderElement<T> = {
|
||||
id: uuid(),
|
||||
component,
|
||||
order,
|
||||
isHidden,
|
||||
getLoaders,
|
||||
};
|
||||
|
||||
if (order === undefined) {
|
||||
|
||||
@@ -17,12 +17,13 @@ import {
|
||||
CachedMapResourceListGetter,
|
||||
CachedMapResourceLoader,
|
||||
CachedMapResourceValue,
|
||||
CachedResource,
|
||||
CachedResourceContext,
|
||||
CachedResourceData,
|
||||
CachedResourceKey,
|
||||
IResource,
|
||||
isResourceKeyList,
|
||||
isResourceKeyListAlias,
|
||||
Resource,
|
||||
ResourceKey,
|
||||
ResourceKeyList,
|
||||
ResourceKeyListAlias,
|
||||
@@ -39,16 +40,11 @@ export interface ResourceKeyWithIncludes<TKey, TIncludes> {
|
||||
readonly includes: TIncludes;
|
||||
}
|
||||
|
||||
type ResourceData<TResource extends CachedResource<any, any, any, any, any>, TKey, TIncludes> = TResource extends CachedDataResource<
|
||||
any,
|
||||
any,
|
||||
any,
|
||||
any
|
||||
>
|
||||
type ResourceData<TResource extends IResource<any, any, any, any>, TKey, TIncludes> = TResource extends CachedDataResource<any, any, any, any>
|
||||
? CachedResourceData<TResource>
|
||||
: CachedMapResourceLoader<TKey, CachedResourceKey<TResource>, CachedResourceData<TResource> extends Map<any, infer I> ? I : never, TIncludes>;
|
||||
|
||||
interface IActions<TResource extends CachedResource<any, any, any, any, any>, TKey, TIncludes> {
|
||||
interface IActions<TResource extends IResource<any, any, any, any>, TKey, TIncludes> {
|
||||
active?: boolean;
|
||||
forceSuspense?: boolean;
|
||||
silent?: boolean;
|
||||
@@ -111,7 +107,7 @@ type TResult<TResource, TKey, TIncludes> = TResource extends CachedDataResource<
|
||||
* @param actions
|
||||
*/
|
||||
export function useResource<
|
||||
TResource extends CachedResource<any, any, any, any, any>,
|
||||
TResource extends IResource<any, any, any, any>,
|
||||
TKeyArg extends ResourceKey<CachedResourceKey<TResource>>,
|
||||
TIncludes extends Readonly<CachedResourceContext<TResource>>,
|
||||
>(
|
||||
@@ -122,7 +118,7 @@ export function useResource<
|
||||
): TResult<TResource, TKeyArg, TIncludes>;
|
||||
|
||||
export function useResource<
|
||||
TResource extends CachedResource<any, any, any, any, any>,
|
||||
TResource extends Resource<any, any, any, any>,
|
||||
TKeyArg extends ResourceKey<CachedResourceKey<TResource>>,
|
||||
TIncludes extends CachedResourceContext<TResource>,
|
||||
>(
|
||||
@@ -132,7 +128,7 @@ export function useResource<
|
||||
actions?: TResource extends any ? IActions<TResource, TKeyArg, TIncludes> : never,
|
||||
): IMapResourceResult<TResource, TIncludes> | IMapResourceListResult<TResource, TIncludes> | IDataResourceResult<TResource, TIncludes> {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const resource = ctor instanceof CachedResource ? ctor : useService(ctor);
|
||||
const resource = ctor instanceof Resource ? ctor : useService(ctor);
|
||||
const errorContext = useContext(ErrorContext);
|
||||
let key: ResourceKey<TKeyArg> | null = keyObj as ResourceKey<TKeyArg>;
|
||||
let includes: TIncludes = [] as unknown as TIncludes;
|
||||
@@ -233,10 +229,10 @@ export function useResource<
|
||||
const { key, includes, resource } = propertiesRef;
|
||||
|
||||
if (refresh) {
|
||||
resource.markOutdated(key);
|
||||
await resource.refresh(key, includes as any);
|
||||
} else {
|
||||
await resource.load(key, includes as any);
|
||||
}
|
||||
|
||||
await resource.load(key, includes as any);
|
||||
},
|
||||
async load(refresh?: boolean): Promise<void> {
|
||||
if (propertiesRef.key === null) {
|
||||
@@ -332,7 +328,7 @@ export function useResource<
|
||||
|
||||
if (!this.isLoaded()) {
|
||||
if (this.loading) {
|
||||
throw this.resource.waitLoad();
|
||||
throw refObj.load();
|
||||
}
|
||||
|
||||
if (this.canLoad) {
|
||||
|
||||
@@ -5,14 +5,15 @@
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { action, computed, makeObservable } from 'mobx';
|
||||
import { action, computed, entries, keys, makeObservable, values } from 'mobx';
|
||||
|
||||
import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor';
|
||||
import { ILoadableState, isArraysEqual, isContainsException } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { CachedResource, CachedResourceKey } from './CachedResource';
|
||||
import { CachedResource } from './CachedResource';
|
||||
import type { CachedResourceIncludeArgs, CachedResourceValueIncludes } from './CachedResourceIncludes';
|
||||
import type { ICachedResourceMetadata } from './ICachedResourceMetadata';
|
||||
import type { CachedResourceKey } from './IResource';
|
||||
import type { ResourceKey, ResourceKeySimple } from './ResourceKey';
|
||||
import type { ResourceKeyAlias } from './ResourceKeyAlias';
|
||||
import { isResourceKeyList, resourceKeyList, ResourceKeyList } from './ResourceKeyList';
|
||||
@@ -46,15 +47,15 @@ export abstract class CachedMapResource<
|
||||
readonly onItemDelete: ISyncExecutor<ResourceKeySimple<TKey>>;
|
||||
|
||||
get entries(): [TKey, TValue][] {
|
||||
return Array.from(this.data.entries());
|
||||
return entries(this.data) as [TKey, TValue][];
|
||||
}
|
||||
|
||||
get values(): TValue[] {
|
||||
return Array.from(this.data.values());
|
||||
return values(this.data) as TValue[];
|
||||
}
|
||||
|
||||
get keys(): TKey[] {
|
||||
return Array.from(this.data.keys());
|
||||
return keys(this.data) as TKey[];
|
||||
}
|
||||
|
||||
constructor(defaultValue?: () => Map<TKey, TValue>, defaultIncludes?: CachedResourceIncludeArgs<TValue, TContext>) {
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { action, makeObservable, observable, toJS } from 'mobx';
|
||||
import { action, makeObservable, observable } from 'mobx';
|
||||
|
||||
import { Dependency } from '@cloudbeaver/core-di';
|
||||
import {
|
||||
ExecutionContext,
|
||||
Executor,
|
||||
@@ -19,7 +18,6 @@ import {
|
||||
SyncExecutor,
|
||||
TaskScheduler,
|
||||
} from '@cloudbeaver/core-executor';
|
||||
import { isPrimitive, MetadataMap } from '@cloudbeaver/core-utils';
|
||||
|
||||
import {
|
||||
CachedResourceOffsetPageKey,
|
||||
@@ -29,30 +27,21 @@ import {
|
||||
isOffsetPageOutdated,
|
||||
} from './CachedResourceOffsetPageKeys';
|
||||
import type { ICachedResourceMetadata } from './ICachedResourceMetadata';
|
||||
import type { IResource } from './IResource';
|
||||
import { Resource } from './Resource';
|
||||
import { isResourceAlias } from './ResourceAlias';
|
||||
import { ResourceAliases } from './ResourceAliases';
|
||||
import { ResourceError } from './ResourceError';
|
||||
import type { ResourceKey, ResourceKeyFlat } from './ResourceKey';
|
||||
import { resourceKeyAlias } from './ResourceKeyAlias';
|
||||
import { isResourceKeyList, resourceKeyList, ResourceKeyList } from './ResourceKeyList';
|
||||
import { resourceKeyList } from './ResourceKeyList';
|
||||
import { resourceKeyListAlias } from './ResourceKeyListAlias';
|
||||
import { ResourceKeyUtils } from './ResourceKeyUtils';
|
||||
import { ResourceLogger } from './ResourceLogger';
|
||||
import { ResourceMetadata } from './ResourceMetadata';
|
||||
import { ResourceOffsetPagination } from './ResourceOffsetPagination';
|
||||
import { ResourceUseTracker } from './ResourceUseTracker';
|
||||
|
||||
export interface IDataError<TKey> {
|
||||
param: ResourceKey<TKey>;
|
||||
exception: Error;
|
||||
}
|
||||
|
||||
export type CachedResourceData<TResource> = TResource extends CachedResource<infer T, any, any, any, any> ? T : never;
|
||||
export type CachedResourceValue<TResource> = TResource extends CachedResource<any, infer T, any, any, any> ? T : never;
|
||||
export type CachedResourceKey<TResource> = TResource extends CachedResource<any, any, infer T, any, any> ? T : never;
|
||||
export type CachedResourceContext<TResource> = TResource extends CachedResource<any, any, any, infer T, any> ? T : void;
|
||||
export type CachedResourceMetadata<TResource> = TResource extends CachedResource<any, any, any, any, infer T> ? T : void;
|
||||
|
||||
export const CachedResourceParamKey = resourceKeyAlias('@cached-resource/param-default');
|
||||
export const CachedResourceListEmptyKey = resourceKeyListAlias('@cached-resource/empty');
|
||||
|
||||
@@ -65,48 +54,33 @@ export abstract class CachedResource<
|
||||
TKey,
|
||||
TInclude extends ReadonlyArray<string>,
|
||||
TMetadata extends ICachedResourceMetadata = ICachedResourceMetadata,
|
||||
> extends Dependency {
|
||||
data: TData;
|
||||
|
||||
> extends Resource<TData, TKey, TInclude, TValue, TMetadata> {
|
||||
readonly onClear: ISyncExecutor;
|
||||
readonly onDataOutdated: ISyncExecutor<ResourceKey<TKey>>;
|
||||
readonly onDataUpdate: ISyncExecutor<ResourceKey<TKey>>;
|
||||
readonly onDataError: ISyncExecutor<IDataError<ResourceKey<TKey>>>;
|
||||
readonly beforeLoad: IExecutor<ResourceKey<TKey>>;
|
||||
readonly useTracker: ResourceUseTracker<TKey, TMetadata>;
|
||||
readonly offsetPagination: ResourceOffsetPagination<TKey, TMetadata>;
|
||||
readonly aliases: ResourceAliases<TKey>;
|
||||
protected defaultIncludes: TInclude;
|
||||
protected get loading(): boolean {
|
||||
return this.scheduler.executing;
|
||||
}
|
||||
|
||||
protected outdateWaitList: ResourceKey<TKey>[];
|
||||
protected readonly scheduler: TaskScheduler<ResourceKey<TKey>>;
|
||||
protected readonly logger: ResourceLogger;
|
||||
protected readonly metadata: ResourceMetadata<TKey, TMetadata>;
|
||||
|
||||
/** Need to infer value type */
|
||||
private readonly typescriptHack: TValue;
|
||||
|
||||
constructor(defaultKey: ResourceKey<TKey>, private readonly defaultValue: () => TData, defaultIncludes: TInclude = [] as any) {
|
||||
super();
|
||||
constructor(defaultKey: ResourceKey<TKey>, defaultValue: () => TData, defaultIncludes: TInclude = [] as any) {
|
||||
super(defaultValue, defaultIncludes);
|
||||
|
||||
this.logger = new ResourceLogger(this.getName());
|
||||
this.aliases = new ResourceAliases(this.logger, this.validateKey.bind(this));
|
||||
this.metadata = new ResourceMetadata(this.aliases, this.getDefaultMetadata.bind(this), this.isKeyEqual.bind(this), this.getKeyRef.bind(this));
|
||||
this.offsetPagination = new ResourceOffsetPagination(this.metadata);
|
||||
this.useTracker = new ResourceUseTracker(this.logger, this.aliases, this.metadata);
|
||||
|
||||
this.isKeyEqual = this.isKeyEqual.bind(this);
|
||||
this.isIntersect = this.isIntersect.bind(this);
|
||||
this.loadingTask = this.loadingTask.bind(this);
|
||||
|
||||
this.typescriptHack = null as any;
|
||||
this.defaultIncludes = defaultIncludes;
|
||||
this.outdateWaitList = [];
|
||||
this.scheduler = new TaskScheduler(this.isIntersect);
|
||||
this.data = defaultValue();
|
||||
this.beforeLoad = new Executor(null, this.isIntersect);
|
||||
this.onClear = new SyncExecutor();
|
||||
this.onDataOutdated = new SyncExecutor<ResourceKey<TKey>>(null);
|
||||
@@ -124,7 +98,6 @@ export abstract class CachedResource<
|
||||
this.logger.spy(this.onDataError, 'onDataError');
|
||||
|
||||
makeObservable<this, 'loader' | 'commitIncludes' | 'resetIncludes' | 'markOutdatedSync'>(this, {
|
||||
data: observable,
|
||||
loader: action,
|
||||
markLoading: action,
|
||||
markLoaded: action,
|
||||
@@ -146,15 +119,11 @@ export abstract class CachedResource<
|
||||
}, 5 * 60 * 1000);
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.constructor.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark resource as in use when {@link resource} is in use
|
||||
* @param resource resource to depend on
|
||||
*/
|
||||
connect(resource: CachedResource<any, any, any, any, any>): void {
|
||||
connect(resource: IResource<any, any, any, any, any>): void {
|
||||
let subscription: string | null = null;
|
||||
|
||||
const subscriptionHandler = () => {
|
||||
@@ -305,18 +274,7 @@ export abstract class CachedResource<
|
||||
}
|
||||
}
|
||||
|
||||
return this.metadata.every(param, metadata => metadata.loaded) && (!includes || this.isIncludes(param, includes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if resource is outdated or not loaded
|
||||
* @param param - Resource key
|
||||
*/
|
||||
isLoadable(param?: ResourceKey<TKey>, context?: TInclude): boolean {
|
||||
if (param === undefined) {
|
||||
param = CachedResourceParamKey;
|
||||
}
|
||||
return !this.isLoaded(param, context) || this.isOutdated(param);
|
||||
return this.metadata.every(param, metadata => metadata.loaded && (!includes || includes.every(include => metadata.includes.includes(include))));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -327,34 +285,6 @@ export abstract class CachedResource<
|
||||
return this.scheduler.wait();
|
||||
}
|
||||
|
||||
isLoading(key?: ResourceKey<TKey>): boolean {
|
||||
if (key === undefined) {
|
||||
key = CachedResourceParamKey;
|
||||
}
|
||||
|
||||
return this.metadata.some(key, metadata => metadata.loading);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if specified {@link includes} is loaded for specified {@link key}
|
||||
* @param key - Resource key
|
||||
* @param includes - Includes
|
||||
*/
|
||||
isIncludes(key: ResourceKey<TKey>, includes: TInclude): boolean {
|
||||
return this.metadata.every(key, metadata => includes.every(include => metadata.includes.includes(include)));
|
||||
}
|
||||
|
||||
getException(param: ResourceKeyFlat<TKey>): Error | null;
|
||||
getException(param: ResourceKeyList<TKey>): Error[] | null;
|
||||
getException(param: ResourceKey<TKey>): Error[] | Error | null;
|
||||
getException(param: ResourceKey<TKey>): Error[] | Error | null {
|
||||
if (isResourceKeyList(param)) {
|
||||
return this.metadata.map(param, metadata => metadata?.exception || null).filter<Error>((exception): exception is Error => exception !== null);
|
||||
}
|
||||
|
||||
return this.metadata.map(param, metadata => metadata?.exception || null);
|
||||
}
|
||||
|
||||
isOutdated(param?: ResourceKey<TKey>): boolean {
|
||||
if (param === undefined) {
|
||||
param = CachedResourceParamKey;
|
||||
@@ -564,55 +494,6 @@ export abstract class CachedResource<
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be overridden to provide equality check for complicated keys
|
||||
*/
|
||||
isKeyEqual(param: TKey, second: TKey): boolean {
|
||||
return param === second;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if key is a part of nextKey
|
||||
* @param nextKey - Resource key
|
||||
* @param key - Resource key
|
||||
* @returns {boolean} Returns true if key can be represented by nextKey
|
||||
*/
|
||||
isIntersect(key: ResourceKey<TKey>, nextKey: ResourceKey<TKey>): boolean {
|
||||
if (key === nextKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isResourceAlias(key) && isResourceAlias(nextKey)) {
|
||||
key = this.aliases.transformToAlias(key);
|
||||
nextKey = this.aliases.transformToAlias(nextKey);
|
||||
|
||||
return key.isEqual(nextKey) && this.isIntersect(key.target, nextKey.target);
|
||||
} else if (isResourceAlias(key) || isResourceAlias(nextKey)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isResourceKeyList(key) || isResourceKeyList(nextKey)) {
|
||||
return ResourceKeyUtils.isIntersect(key, nextKey, this.isKeyEqual);
|
||||
}
|
||||
|
||||
return ResourceKeyUtils.isIntersect(key, nextKey, this.isKeyEqual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be overridden to provide static link to complicated keys
|
||||
*/
|
||||
protected getKeyRef(key: TKey): TKey {
|
||||
if (isPrimitive(key)) {
|
||||
return key;
|
||||
}
|
||||
return Object.freeze(toJS(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if key is valid. Can be overridden to provide custom validation.
|
||||
*/
|
||||
protected abstract validateKey(key: TKey): boolean;
|
||||
|
||||
protected resetIncludes(): void {
|
||||
this.metadata.update(metadata => {
|
||||
metadata.includes = observable([...this.defaultIncludes]);
|
||||
@@ -670,21 +551,6 @@ export abstract class CachedResource<
|
||||
this.onDataOutdated.execute(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use to extend metadata
|
||||
* @returns {Record<string, any>} Object Map
|
||||
*/
|
||||
protected getDefaultMetadata(key: TKey, metadata: MetadataMap<TKey, TMetadata>): TMetadata {
|
||||
return {
|
||||
loaded: false,
|
||||
outdated: true,
|
||||
loading: false,
|
||||
exception: null,
|
||||
includes: observable([...this.defaultIncludes]),
|
||||
dependencies: observable([]),
|
||||
} as ICachedResourceMetadata as TMetadata;
|
||||
}
|
||||
|
||||
protected async preLoadData(
|
||||
param: ResourceKey<TKey>,
|
||||
contexts: IExecutionContextProvider<ResourceKey<TKey>>,
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 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 type { ICachedResourceMetadata } from './ICachedResourceMetadata';
|
||||
import type { ResourceAliases } from './ResourceAliases';
|
||||
import type { ResourceKey, ResourceKeyFlat } from './ResourceKey';
|
||||
import type { ResourceKeyList } from './ResourceKeyList';
|
||||
import type { ResourceUseTracker } from './ResourceUseTracker';
|
||||
|
||||
export type CachedResourceData<TResource> = TResource extends IResource<infer T, any, any, any, any> ? T : never;
|
||||
export type CachedResourceKey<TResource> = TResource extends IResource<any, infer T, any, any, any> ? T : never;
|
||||
export type CachedResourceContext<TResource> = TResource extends IResource<any, any, infer T, any, any> ? T : void;
|
||||
export type CachedResourceValue<TResource> = TResource extends IResource<any, any, any, infer T, any> ? T : never;
|
||||
export type CachedResourceMetadata<TResource> = TResource extends IResource<any, any, any, any, infer T> ? T : void;
|
||||
|
||||
export interface IResource<
|
||||
TData,
|
||||
TKey,
|
||||
TInclude extends ReadonlyArray<string>,
|
||||
TValue = TData,
|
||||
TMetadata extends ICachedResourceMetadata = ICachedResourceMetadata,
|
||||
> {
|
||||
data: TData;
|
||||
readonly aliases: ResourceAliases<TKey>;
|
||||
readonly useTracker: ResourceUseTracker<TKey, TMetadata>;
|
||||
getName(): string;
|
||||
|
||||
getException(param: ResourceKeyFlat<TKey>): Error | null;
|
||||
getException(param: ResourceKeyList<TKey>): Error[] | null;
|
||||
getException(param: ResourceKey<TKey>): Error[] | Error | null;
|
||||
getException(param: ResourceKey<TKey>): Error[] | Error | null;
|
||||
|
||||
isLoadable(param?: ResourceKey<TKey>, context?: TInclude): boolean;
|
||||
isLoaded(param?: ResourceKey<TKey>, includes?: TInclude): boolean;
|
||||
isLoading(key?: ResourceKey<TKey>): boolean;
|
||||
isOutdated(param?: ResourceKey<TKey>): boolean;
|
||||
isIntersect(key: ResourceKey<TKey>, nextKey: ResourceKey<TKey>): boolean;
|
||||
|
||||
load(key?: ResourceKey<TKey>, context?: TInclude): Promise<TValue>;
|
||||
refresh(key?: ResourceKey<TKey>, context?: TInclude): Promise<TValue>;
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 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 { makeObservable, observable, toJS } from 'mobx';
|
||||
|
||||
import { Dependency } from '@cloudbeaver/core-di';
|
||||
import { isContainsException, isPrimitive, MetadataMap } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { CachedResourceParamKey } from './CachedResource';
|
||||
import type { ICachedResourceMetadata } from './ICachedResourceMetadata';
|
||||
import type { IResource } from './IResource';
|
||||
import { isResourceAlias } from './ResourceAlias';
|
||||
import { ResourceAliases } from './ResourceAliases';
|
||||
import type { ResourceKey, ResourceKeyFlat } from './ResourceKey';
|
||||
import { isResourceKeyList, type ResourceKeyList } from './ResourceKeyList';
|
||||
import { ResourceKeyUtils } from './ResourceKeyUtils';
|
||||
import { ResourceLogger } from './ResourceLogger';
|
||||
import { ResourceMetadata } from './ResourceMetadata';
|
||||
import { ResourceUseTracker } from './ResourceUseTracker';
|
||||
|
||||
export abstract class Resource<
|
||||
TData,
|
||||
TKey,
|
||||
TInclude extends ReadonlyArray<string>,
|
||||
TValue = TData,
|
||||
TMetadata extends ICachedResourceMetadata = ICachedResourceMetadata,
|
||||
>
|
||||
extends Dependency
|
||||
implements IResource<TData, TKey, TInclude, TValue, TMetadata>
|
||||
{
|
||||
data: TData;
|
||||
|
||||
readonly aliases: ResourceAliases<TKey>;
|
||||
readonly useTracker: ResourceUseTracker<TKey, TMetadata>;
|
||||
|
||||
protected readonly logger: ResourceLogger;
|
||||
protected readonly metadata: ResourceMetadata<TKey, TMetadata>;
|
||||
|
||||
constructor(protected readonly defaultValue: () => TData, protected defaultIncludes: TInclude = [] as any) {
|
||||
super();
|
||||
this.isKeyEqual = this.isKeyEqual.bind(this);
|
||||
this.isIntersect = this.isIntersect.bind(this);
|
||||
|
||||
this.logger = new ResourceLogger(this.getName());
|
||||
this.aliases = new ResourceAliases(this.logger, this.validateKey.bind(this));
|
||||
this.metadata = new ResourceMetadata(this.aliases, this.getDefaultMetadata.bind(this), this.isKeyEqual, this.getKeyRef.bind(this));
|
||||
this.useTracker = new ResourceUseTracker(this.logger, this.aliases, this.metadata);
|
||||
|
||||
this.data = this.defaultValue();
|
||||
|
||||
makeObservable<this>(this, {
|
||||
data: observable,
|
||||
});
|
||||
}
|
||||
|
||||
abstract isLoaded(param?: ResourceKey<TKey> | undefined, includes?: TInclude | undefined): boolean;
|
||||
abstract isOutdated(param?: ResourceKey<TKey> | undefined): boolean;
|
||||
|
||||
isLoadable(param?: ResourceKey<TKey> | undefined, context?: TInclude | undefined): boolean {
|
||||
if (param === undefined) {
|
||||
param = CachedResourceParamKey;
|
||||
}
|
||||
|
||||
if (isContainsException(this.getException(param))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !this.isLoaded(param, context) || this.isOutdated(param);
|
||||
}
|
||||
|
||||
isLoading(key?: ResourceKey<TKey>): boolean {
|
||||
if (key === undefined) {
|
||||
key = CachedResourceParamKey;
|
||||
}
|
||||
|
||||
return this.metadata.some(key, metadata => metadata.loading);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if key is a part of nextKey
|
||||
* @param nextKey - Resource key
|
||||
* @param key - Resource key
|
||||
* @returns {boolean} Returns true if key can be represented by nextKey
|
||||
*/
|
||||
isIntersect(key: ResourceKey<TKey>, nextKey: ResourceKey<TKey>): boolean {
|
||||
if (key === nextKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isResourceAlias(key) && isResourceAlias(nextKey)) {
|
||||
key = this.aliases.transformToAlias(key);
|
||||
nextKey = this.aliases.transformToAlias(nextKey);
|
||||
|
||||
return key.isEqual(nextKey) && this.isIntersect(key.target, nextKey.target);
|
||||
} else if (isResourceAlias(key) || isResourceAlias(nextKey)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isResourceKeyList(key) || isResourceKeyList(nextKey)) {
|
||||
return ResourceKeyUtils.isIntersect(key, nextKey, this.isKeyEqual);
|
||||
}
|
||||
|
||||
return ResourceKeyUtils.isIntersect(key, nextKey, this.isKeyEqual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be overridden to provide equality check for complicated keys
|
||||
*/
|
||||
isKeyEqual(param: TKey, second: TKey): boolean {
|
||||
return param === second;
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.constructor.name;
|
||||
}
|
||||
|
||||
getException(param: ResourceKeyFlat<TKey>): Error | null;
|
||||
getException(param: ResourceKeyList<TKey>): Error[] | null;
|
||||
getException(param: ResourceKey<TKey>): Error[] | Error | null;
|
||||
getException(param: ResourceKey<TKey>): Error[] | Error | null {
|
||||
if (isResourceKeyList(param)) {
|
||||
return this.metadata.map(param, metadata => metadata?.exception || null).filter<Error>((exception): exception is Error => exception !== null);
|
||||
}
|
||||
|
||||
return this.metadata.map(param, metadata => metadata?.exception || null);
|
||||
}
|
||||
|
||||
abstract load(key?: ResourceKey<TKey> | undefined, context?: TInclude | undefined): Promise<TValue>;
|
||||
abstract refresh(key?: ResourceKey<TKey> | undefined, context?: TInclude | undefined): Promise<TValue>;
|
||||
|
||||
/**
|
||||
* Can be overridden to provide static link to complicated keys
|
||||
*/
|
||||
protected getKeyRef(key: TKey): TKey {
|
||||
if (isPrimitive(key)) {
|
||||
return key;
|
||||
}
|
||||
return Object.freeze(toJS(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if key is valid. Can be overridden to provide custom validation.
|
||||
*/
|
||||
protected abstract validateKey(key: TKey): boolean;
|
||||
|
||||
/**
|
||||
* Use to extend metadata
|
||||
* @returns {Record<string, any>} Object Map
|
||||
*/
|
||||
protected getDefaultMetadata(key: TKey, metadata: MetadataMap<TKey, TMetadata>): TMetadata {
|
||||
return {
|
||||
loaded: false,
|
||||
outdated: true,
|
||||
loading: false,
|
||||
exception: null,
|
||||
includes: observable([...this.defaultIncludes]),
|
||||
dependencies: observable([]),
|
||||
} as ICachedResourceMetadata as TMetadata;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { action } from 'mobx';
|
||||
|
||||
import { LoadingError } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { CachedResource } from './CachedResource';
|
||||
@@ -18,12 +20,12 @@ export class ResourceError extends LoadingError {
|
||||
options?: ErrorOptions,
|
||||
) {
|
||||
super(
|
||||
() => {
|
||||
action(() => {
|
||||
// @TODO extract clean error logic to the CachedResource.
|
||||
// For now when the ResourceError is thrown and refresh fn is called, the error is not cleaned in the resource
|
||||
this.resource.cleanError(this.key);
|
||||
this.resource.markOutdated(this.key);
|
||||
},
|
||||
}),
|
||||
message,
|
||||
options,
|
||||
);
|
||||
|
||||
@@ -50,16 +50,31 @@ export class ResourceMetadata<TKey, TMetadata extends ICachedResourceMetadata> {
|
||||
return this.metadata.has(this.getMetadataKeyRef(key));
|
||||
}
|
||||
|
||||
every(param: ResourceKey<TKey>, predicate: (metadata: TMetadata) => boolean): boolean {
|
||||
every(predicate: (metadata: TMetadata) => boolean): boolean;
|
||||
every(param: ResourceKey<TKey>, predicate: (metadata: TMetadata) => boolean): boolean;
|
||||
every(param: ResourceKey<TKey> | ((metadata: TMetadata) => boolean), predicate?: (metadata: TMetadata) => boolean): boolean {
|
||||
if (!predicate) {
|
||||
predicate = param as (metadata: TMetadata) => boolean;
|
||||
for (const metadata of this.values()) {
|
||||
if (!predicate(metadata)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
param = param as ResourceKey<TKey>;
|
||||
predicate = predicate as MetadataCallback<TMetadata, boolean>;
|
||||
|
||||
if (!this.has(param)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !this.some(param, key => !predicate(key));
|
||||
return !this.some(param, key => !predicate!(key));
|
||||
}
|
||||
|
||||
some(predicate: MetadataCallback<TMetadata, boolean>): boolean;
|
||||
some(param: ResourceKey<TKey>, predicate: MetadataCallback<TMetadata, boolean>): boolean;
|
||||
|
||||
some(param: ResourceKey<TKey> | MetadataCallback<TMetadata, boolean>, predicate?: MetadataCallback<TMetadata, boolean>): boolean {
|
||||
if (!predicate) {
|
||||
predicate = param as (metadata: TMetadata) => boolean;
|
||||
|
||||
@@ -12,6 +12,8 @@ export {
|
||||
} from './Resource/CachedResourceOffsetPageKeys';
|
||||
export * from './Resource/CachedTreeResource/CachedTreeResource';
|
||||
export * from './Resource/ICachedResourceMetadata';
|
||||
export * from './Resource/IResource';
|
||||
export * from './Resource/Resource';
|
||||
export * from './Resource/ResourceAlias';
|
||||
export * from './Resource/ResourceError';
|
||||
export * from './Resource/ResourceKey';
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { Connectable, connectable, filter, map, merge, Observable, Subject } from 'rxjs';
|
||||
|
||||
import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor';
|
||||
import type { CachedResource } from '@cloudbeaver/core-resource';
|
||||
import type { IResource } from '@cloudbeaver/core-resource';
|
||||
import { compose } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { IBaseServerEvent, IServerEventCallback, IServerEventEmitter, Subscription } from './IServerEventEmitter';
|
||||
@@ -29,8 +29,8 @@ export abstract class TopicEventHandler<
|
||||
readonly eventsSubject: Connectable<TEvent>;
|
||||
|
||||
private subscription: Subscription | null;
|
||||
private readonly activeResources: Array<CachedResource<any, any, any, any, any>>;
|
||||
private readonly subscribedResources: Map<CachedResource<any, any, any, any, any>, ISubscribedResourceInfo>;
|
||||
private readonly activeResources: Array<IResource<any, any, any, any, any>>;
|
||||
private readonly subscribedResources: Map<IResource<any, any, any, any, any>, ISubscribedResourceInfo>;
|
||||
private readonly serverSubject?: Observable<TEvent>;
|
||||
private readonly subject: Subject<TEvent>;
|
||||
constructor(private readonly topic: string, private readonly emitter: IServerEventEmitter<SourceEvent>) {
|
||||
@@ -56,7 +56,7 @@ export abstract class TopicEventHandler<
|
||||
id: TEventID,
|
||||
callback: IServerEventCallback<T>,
|
||||
mapTo: (event: TEvent) => T = event => event as unknown as T,
|
||||
resource?: CachedResource<any, any, any, any, any>,
|
||||
resource?: IResource<any, any, any, any, any>,
|
||||
): Subscription {
|
||||
if (resource) {
|
||||
this.registerResource(resource);
|
||||
@@ -82,7 +82,7 @@ export abstract class TopicEventHandler<
|
||||
callback: IServerEventCallback<T>,
|
||||
mapTo: (param: TEvent) => T = event => event as unknown as T,
|
||||
filterFn: (param: TEvent) => boolean = () => true,
|
||||
resource?: CachedResource<any, any, any, any, any>,
|
||||
resource?: IResource<any, any, any, any, any>,
|
||||
): Subscription {
|
||||
if (resource) {
|
||||
this.registerResource(resource);
|
||||
@@ -104,7 +104,7 @@ export abstract class TopicEventHandler<
|
||||
return this;
|
||||
}
|
||||
|
||||
private resourceUseHandler(resource: CachedResource<any, any, any, any, any>) {
|
||||
private resourceUseHandler(resource: IResource<any, any, any, any, any>) {
|
||||
const index = this.activeResources.indexOf(resource);
|
||||
|
||||
if (index !== -1) {
|
||||
@@ -124,7 +124,7 @@ export abstract class TopicEventHandler<
|
||||
}
|
||||
}
|
||||
|
||||
private removeActiveResource(resource: CachedResource<any, any, any, any, any>) {
|
||||
private removeActiveResource(resource: IResource<any, any, any, any, any>) {
|
||||
this.activeResources.splice(this.activeResources.indexOf(resource), 1);
|
||||
|
||||
if (this.activeResources.length === 0) {
|
||||
@@ -134,7 +134,7 @@ export abstract class TopicEventHandler<
|
||||
}
|
||||
}
|
||||
|
||||
private registerResource(resource: CachedResource<any, any, any, any, any>): void {
|
||||
private registerResource(resource: IResource<any, any, any, any, any>): void {
|
||||
let info = this.subscribedResources.get(resource);
|
||||
|
||||
if (!info) {
|
||||
@@ -150,7 +150,7 @@ export abstract class TopicEventHandler<
|
||||
info.listeners++;
|
||||
}
|
||||
|
||||
private removeResource(resource: CachedResource<any, any, any, any, any>): void {
|
||||
private removeResource(resource: IResource<any, any, any, any, any>): void {
|
||||
const info = this.subscribedResources.get(resource);
|
||||
|
||||
if (info) {
|
||||
|
||||
@@ -123,19 +123,19 @@ function filterApplicable(contexts: IDataContextProvider): (creator: IMenuItemsC
|
||||
const local = contexts.get(DATA_CONTEXT_MENU_LOCAL);
|
||||
|
||||
return (creator: IMenuItemsCreator) => {
|
||||
if (creator.menus) {
|
||||
const applicable = creator.menus.some(menu => contexts.hasValue(DATA_CONTEXT_MENU, menu, false));
|
||||
|
||||
if (!applicable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (local) {
|
||||
if (!creator.menus && !creator.contexts) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (creator.menus) {
|
||||
const applicable = creator.menus.some(menu => contexts.hasValue(DATA_CONTEXT_MENU, menu, false));
|
||||
|
||||
if (!applicable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (creator.contexts) {
|
||||
const applicable = creator.contexts.some(context => contexts.has(context));
|
||||
|
||||
@@ -151,14 +151,6 @@ function filterApplicable(contexts: IDataContextProvider): (creator: IMenuItemsC
|
||||
return false;
|
||||
}
|
||||
|
||||
if (creator.menus) {
|
||||
const applicable = creator.menus.some(menu => contexts.hasValue(DATA_CONTEXT_MENU, menu, false));
|
||||
|
||||
if (!applicable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -147,11 +147,9 @@ export const Administration = observer<React.PropsWithChildren<Props>>(function
|
||||
</Loader>
|
||||
</SlideElement>
|
||||
<SlideElement>
|
||||
<Loader suspense>
|
||||
<content>
|
||||
<ItemContent activeScreen={activeScreen} configurationWizard={configurationWizard} />
|
||||
</content>
|
||||
</Loader>
|
||||
<content>
|
||||
<ItemContent activeScreen={activeScreen} configurationWizard={configurationWizard} />
|
||||
</content>
|
||||
<SlideOverlay onClick={() => optionsPanelService.close()} />
|
||||
</SlideElement>
|
||||
</SlideBox>
|
||||
|
||||
@@ -39,11 +39,19 @@ export const ItemContent = observer<Props>(function ItemContent({ activeScreen,
|
||||
if (sub) {
|
||||
const Component = sub.getComponent ? sub.getComponent() : item.getContentComponent();
|
||||
|
||||
return <Component item={item} sub={sub} param={activeScreen.param} configurationWizard={configurationWizard} />;
|
||||
return (
|
||||
<Loader suspense>
|
||||
<Component item={item} sub={sub} param={activeScreen.param} configurationWizard={configurationWizard} />
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Component = item.getContentComponent();
|
||||
|
||||
return <Component item={item} configurationWizard={configurationWizard} />;
|
||||
return (
|
||||
<Loader suspense>
|
||||
<Component item={item} configurationWizard={configurationWizard} />
|
||||
</Loader>
|
||||
);
|
||||
});
|
||||
|
||||
+2
-1
@@ -13,6 +13,7 @@ import { Combobox, Group, GroupTitle, ITag, s, Tag, Tags, useResource, useS, use
|
||||
import { DBDriverResource } from '@cloudbeaver/core-connections';
|
||||
import { CachedMapAllKey, resourceKeyList } from '@cloudbeaver/core-resource';
|
||||
import type { ServerConfigInput } from '@cloudbeaver/core-sdk';
|
||||
import { isDefined } from '@cloudbeaver/core-utils';
|
||||
|
||||
import style from './ServerConfigurationDriversForm.m.css';
|
||||
|
||||
@@ -25,7 +26,7 @@ export const ServerConfigurationDriversForm = observer<Props>(function ServerCon
|
||||
const translate = useTranslate();
|
||||
const driversResource = useResource(ServerConfigurationDriversForm, DBDriverResource, CachedMapAllKey);
|
||||
|
||||
const drivers = driversResource.resource.values.slice().sort(driversResource.resource.compare);
|
||||
const drivers = driversResource.data.filter(isDefined).sort(driversResource.resource.compare);
|
||||
|
||||
const tags: ITag[] = driversResource.resource
|
||||
.get(resourceKeyList(serverConfig.disabledDrivers || []))
|
||||
|
||||
@@ -3,8 +3,8 @@ export default [
|
||||
['administration_server_configuration_save_confirmation_message', 'You are about to change critical settings. Are you sure?'],
|
||||
|
||||
['administration_configuration_wizard_welcome', 'Welcome'],
|
||||
['administration_configuration_wizard_welcome_step_description', 'Welcome to CloudBeaver'],
|
||||
['administration_configuration_wizard_welcome_title', 'Welcome to CloudBeaver, cloud database management system!'],
|
||||
['administration_configuration_wizard_welcome_step_description', 'Welcome to {alias:product_full_name}'],
|
||||
['administration_configuration_wizard_welcome_title', 'Welcome to {alias:product_full_name}, cloud database management system!'],
|
||||
[
|
||||
'administration_configuration_wizard_welcome_message',
|
||||
'The easy configuration wizard will guide you through several simple steps to set up the CloudBeaver server. You will need to set server information and administrator credentials. You can set up additional server parameters once the easy configuration is completed.',
|
||||
|
||||
+5
-2
@@ -11,6 +11,7 @@ import { ColoredContainer, Container, FieldCheckbox, Group, GroupTitle, Placehol
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { TabContainerPanelComponent, useTab, useTabState } from '@cloudbeaver/core-ui';
|
||||
|
||||
import { AdministrationUsersManagementService } from '../../../../AdministrationUsersManagementService';
|
||||
import type { UserFormProps } from '../AdministrationUserFormService';
|
||||
import { UserFormInfoCredentials } from './UserFormInfoCredentials';
|
||||
import { UserFormInfoMetaParameters } from './UserFormInfoMetaParameters';
|
||||
@@ -23,10 +24,12 @@ export const UserFormInfo: TabContainerPanelComponent<UserFormProps> = observer(
|
||||
const tab = useTab(tabId);
|
||||
const tabState = useTabState<UserFormInfoPart>();
|
||||
const userFormInfoPartService = useService(UserFormInfoPartService);
|
||||
const administrationUsersManagementService = useService(AdministrationUsersManagementService);
|
||||
|
||||
useAutoLoad(UserFormInfo, tabState, tab.selected);
|
||||
useAutoLoad(UserFormInfo, [tabState, ...administrationUsersManagementService.loaders], tab.selected);
|
||||
|
||||
const disabled = tabState.isLoading();
|
||||
const userManagementDisabled = administrationUsersManagementService.externalUserProviderEnabled;
|
||||
// let info: TLocalizationToken | null = null;
|
||||
|
||||
// if (formState.mode === FormMode.Edit && tabState.isChanged()) {
|
||||
@@ -43,7 +46,7 @@ export const UserFormInfo: TabContainerPanelComponent<UserFormProps> = observer(
|
||||
<Group small gap overflow>
|
||||
<Placeholder container={userFormInfoPartService.placeholderContainer} formState={formState} />
|
||||
<GroupTitle>{translate('authentication_user_status')}</GroupTitle>
|
||||
<FieldCheckbox id={`${formState.id}_user_enabled`} name="enabled" state={tabState.state} disabled={disabled}>
|
||||
<FieldCheckbox id={`${formState.id}_user_enabled`} name="enabled" state={tabState.state} disabled={disabled || userManagementDisabled}>
|
||||
{translate('authentication_user_enabled')}
|
||||
</FieldCheckbox>
|
||||
<UserFormInfoTeams formState={formState} tabState={tabState} tabSelected={tab.selected} disabled={disabled} />
|
||||
|
||||
+7
-4
@@ -11,6 +11,7 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
import { CachedMapAllKey, getCachedMapResourceLoaderState } from '@cloudbeaver/core-resource';
|
||||
import { ACTION_CREATE, ActionService, MenuService } from '@cloudbeaver/core-view';
|
||||
|
||||
import { AdministrationUsersManagementService } from '../../../AdministrationUsersManagementService';
|
||||
import { MENU_USERS_ADMINISTRATION } from '../../../Menus/MENU_USERS_ADMINISTRATION';
|
||||
import { ADMINISTRATION_ITEM_USER_CREATE_PARAM } from '../ADMINISTRATION_ITEM_USER_CREATE_PARAM';
|
||||
import { CreateUserService } from './CreateUserService';
|
||||
@@ -22,6 +23,7 @@ export class CreateUserBootstrap extends Bootstrap {
|
||||
private readonly createUserService: CreateUserService,
|
||||
private readonly menuService: MenuService,
|
||||
private readonly actionService: ActionService,
|
||||
private readonly administrationUsersManagementService: AdministrationUsersManagementService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -37,7 +39,7 @@ export class CreateUserBootstrap extends Bootstrap {
|
||||
this.actionService.addHandler({
|
||||
id: 'users-table-base',
|
||||
isActionApplicable: (context, action) => {
|
||||
if (action === ACTION_CREATE) {
|
||||
if (action === ACTION_CREATE && !this.administrationUsersManagementService.externalUserProviderEnabled) {
|
||||
return this.authProvidersResource.has(AUTH_PROVIDER_LOCAL_ID);
|
||||
}
|
||||
|
||||
@@ -52,9 +54,10 @@ export class CreateUserBootstrap extends Bootstrap {
|
||||
|
||||
return false;
|
||||
},
|
||||
getLoader: (context, action) => {
|
||||
return getCachedMapResourceLoaderState(this.authProvidersResource, () => CachedMapAllKey);
|
||||
},
|
||||
getLoader: () => [
|
||||
getCachedMapResourceLoaderState(this.authProvidersResource, () => CachedMapAllKey),
|
||||
...this.administrationUsersManagementService.loaders,
|
||||
],
|
||||
handler: (context, action) => {
|
||||
switch (action) {
|
||||
case ACTION_CREATE:
|
||||
|
||||
+18
-2
@@ -9,10 +9,21 @@ import { observer } from 'mobx-react-lite';
|
||||
import styled, { css, use } from 'reshadow';
|
||||
|
||||
import { AdminUser, UsersResource } from '@cloudbeaver/core-authentication';
|
||||
import { Checkbox, Loader, Placeholder, TableColumnValue, TableItem, TableItemExpand, TableItemSelect, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import {
|
||||
Checkbox,
|
||||
Loader,
|
||||
Placeholder,
|
||||
TableColumnValue,
|
||||
TableItem,
|
||||
TableItemExpand,
|
||||
TableItemSelect,
|
||||
useAutoLoad,
|
||||
useTranslate,
|
||||
} from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
|
||||
import { AdministrationUsersManagementService } from '../../../AdministrationUsersManagementService';
|
||||
import { UsersAdministrationService } from '../UsersAdministrationService';
|
||||
import { UserEdit } from './UserEdit';
|
||||
|
||||
@@ -36,8 +47,11 @@ export const User = observer<Props>(function User({ user, displayAuthRole, selec
|
||||
const teams = user.grantedTeams.join(', ');
|
||||
const usersService = useService(UsersResource);
|
||||
const notificationService = useService(NotificationService);
|
||||
const administrationUsersManagementService = useService(AdministrationUsersManagementService);
|
||||
const translate = useTranslate();
|
||||
|
||||
useAutoLoad(User, administrationUsersManagementService.loaders);
|
||||
|
||||
async function handleEnabledCheckboxChange(enabled: boolean) {
|
||||
try {
|
||||
await usersService.enableUser(user.userId, enabled);
|
||||
@@ -50,6 +64,8 @@ export const User = observer<Props>(function User({ user, displayAuthRole, selec
|
||||
? translate('administration_teams_team_granted_users_permission_denied')
|
||||
: undefined;
|
||||
|
||||
const userManagementDisabled = administrationUsersManagementService.externalUserProviderEnabled;
|
||||
|
||||
return styled(styles)(
|
||||
<TableItem item={user.userId} expandElement={UserEdit} selectDisabled={!selectable}>
|
||||
{selectable && (
|
||||
@@ -74,7 +90,7 @@ export const User = observer<Props>(function User({ user, displayAuthRole, selec
|
||||
<TableColumnValue>
|
||||
<Checkbox
|
||||
checked={user.enabled}
|
||||
disabled={usersService.isActiveUser(user.userId)}
|
||||
disabled={usersService.isActiveUser(user.userId) || userManagementDisabled}
|
||||
title={enabledCheckboxTitle}
|
||||
onChange={handleEnabledCheckboxChange}
|
||||
/>
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { s, SContext, StyleRegistry, ToolsAction, ToolsPanel, useResource, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import { s, SContext, StyleRegistry, ToolsAction, ToolsPanel, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import { MenuBar, MenuBarItemStyles } from '@cloudbeaver/core-ui';
|
||||
import { useMenu } from '@cloudbeaver/core-view';
|
||||
|
||||
|
||||
+6
-2
@@ -10,9 +10,10 @@ import styled from 'reshadow';
|
||||
|
||||
import { ADMINISTRATION_TOOLS_PANEL_STYLES, IAdministrationItemSubItem } from '@cloudbeaver/core-administration';
|
||||
import { AuthRolesResource } from '@cloudbeaver/core-authentication';
|
||||
import { ColoredContainer, Container, Group, Placeholder, useResource, useStyles } from '@cloudbeaver/core-blocks';
|
||||
import { ColoredContainer, Container, Group, Placeholder, useAutoLoad, useResource, useStyles } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
|
||||
import { AdministrationUsersManagementService } from '../../../AdministrationUsersManagementService';
|
||||
import { CreateUser } from './CreateUser';
|
||||
import { CreateUserService } from './CreateUserService';
|
||||
import { UsersTableFilters } from './Filters/UsersTableFilters';
|
||||
@@ -30,13 +31,16 @@ export const UsersPage = observer<Props>(function UsersPage({ sub, param }) {
|
||||
const style = useStyles(ADMINISTRATION_TOOLS_PANEL_STYLES);
|
||||
const createUserService = useService(CreateUserService);
|
||||
const authRolesResource = useResource(UsersPage, AuthRolesResource, undefined);
|
||||
const administrationUsersManagementService = useService(AdministrationUsersManagementService);
|
||||
|
||||
useAutoLoad(UsersPage, administrationUsersManagementService.loaders);
|
||||
const filters = useUsersTableFilters();
|
||||
const table = useUsersTable(filters);
|
||||
|
||||
const create = param === 'create';
|
||||
const displayAuthRole = authRolesResource.data.length > 0;
|
||||
const loading = authRolesResource.isLoading() || table.loadableState.isLoading();
|
||||
const userManagementDisabled = administrationUsersManagementService.externalUserProviderEnabled;
|
||||
|
||||
return styled(style)(
|
||||
<ColoredContainer vertical wrap gap parent>
|
||||
@@ -45,7 +49,7 @@ export const UsersPage = observer<Props>(function UsersPage({ sub, param }) {
|
||||
</Group>
|
||||
|
||||
<Container overflow gap>
|
||||
{create && createUserService.state && (
|
||||
{create && createUserService.state && !userManagementDisabled && (
|
||||
<Group box>
|
||||
<CreateUser state={createUserService.state} onCancel={createUserService.cancelCreate} />
|
||||
</Group>
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 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 { computed, makeObservable } from 'mobx';
|
||||
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { type ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor';
|
||||
import { type ILoadableState, isArraysEqual } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { externalUserProviderStatusContext } from './externalUserProviderStatusContext';
|
||||
|
||||
@injectable()
|
||||
export class AdministrationUsersManagementService {
|
||||
get externalUserProviderEnabled(): boolean {
|
||||
const contexts = this.getExternalUserProviderStatus.execute();
|
||||
const projectsContext = contexts.getContext(externalUserProviderStatusContext);
|
||||
|
||||
return projectsContext.externalUserProviderEnabled;
|
||||
}
|
||||
|
||||
get loaders(): ILoadableState[] {
|
||||
const contexts = this.getExternalUserProviderStatus.execute();
|
||||
const projectsContext = contexts.getContext(externalUserProviderStatusContext);
|
||||
|
||||
return projectsContext.loaders;
|
||||
}
|
||||
|
||||
readonly getExternalUserProviderStatus: ISyncExecutor;
|
||||
|
||||
constructor() {
|
||||
makeObservable(this, {
|
||||
externalUserProviderEnabled: computed,
|
||||
loaders: computed<ILoadableState[]>({ equals: (a, b) => isArraysEqual(a, b) }),
|
||||
});
|
||||
|
||||
this.getExternalUserProviderStatus = new SyncExecutor();
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 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 type { ILoadableState } from '@cloudbeaver/core-utils';
|
||||
|
||||
interface IExternalUserProviderStatusContext {
|
||||
externalUserProviderEnabled: boolean;
|
||||
loaders: ILoadableState[];
|
||||
setExternalUserProviderStatus(enabled: boolean): void;
|
||||
addLoader(loader: ILoadableState): void;
|
||||
}
|
||||
|
||||
export function externalUserProviderStatusContext(): IExternalUserProviderStatusContext {
|
||||
return {
|
||||
externalUserProviderEnabled: false,
|
||||
loaders: [],
|
||||
setExternalUserProviderStatus(enabled: boolean) {
|
||||
this.externalUserProviderEnabled = enabled;
|
||||
},
|
||||
addLoader(loader: ILoadableState) {
|
||||
this.loaders.push(loader);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -17,3 +17,5 @@ export * from './Administration/Users/UserForm/Info/UserFormInfoPartService';
|
||||
export * from './Administration/IdentityProviders/IAuthConfigurationFormProps';
|
||||
export * from './Administration/IdentityProviders/AuthConfigurationFormService';
|
||||
export * from './Menus/MENU_USERS_ADMINISTRATION';
|
||||
export * from './AdministrationUsersManagementService';
|
||||
export * from './externalUserProviderStatusContext';
|
||||
|
||||
@@ -30,6 +30,7 @@ import { UsersAdministrationNavigationService } from './Administration/Users/Use
|
||||
import { UsersAdministrationService } from './Administration/Users/UsersAdministrationService';
|
||||
import { CreateUserBootstrap } from './Administration/Users/UsersTable/CreateUserBootstrap';
|
||||
import { CreateUserService } from './Administration/Users/UsersTable/CreateUserService';
|
||||
import { AdministrationUsersManagementService } from './AdministrationUsersManagementService';
|
||||
import { AuthenticationLocaleService } from './AuthenticationLocaleService';
|
||||
import { PluginBootstrap } from './PluginBootstrap';
|
||||
|
||||
@@ -64,5 +65,6 @@ export const manifest: PluginManifest = {
|
||||
UserFormOriginPartBootstrap,
|
||||
UserFormConnectionAccessPartBootstrap,
|
||||
UserFormInfoPartService,
|
||||
AdministrationUsersManagementService,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import { LocalStorageSaveService } from '@cloudbeaver/core-settings';
|
||||
interface IDevToolsSettings {
|
||||
enabled: boolean;
|
||||
distributed: boolean;
|
||||
configuration: boolean;
|
||||
}
|
||||
|
||||
const DEVTOOLS = 'devtools';
|
||||
@@ -28,6 +29,10 @@ export class DevToolsService {
|
||||
return this.settings.distributed;
|
||||
}
|
||||
|
||||
get isConfiguration(): boolean {
|
||||
return this.settings.configuration;
|
||||
}
|
||||
|
||||
private readonly settings: IDevToolsSettings;
|
||||
|
||||
constructor(private readonly serverConfigResource: ServerConfigResource, private readonly autoSaveService: LocalStorageSaveService) {
|
||||
@@ -37,23 +42,29 @@ export class DevToolsService {
|
||||
settings: observable,
|
||||
});
|
||||
this.autoSaveService.withAutoSave(DEVTOOLS, this.settings, getDefaultDevToolsSettings);
|
||||
this.serverConfigResource.onDataUpdate.addHandler(this.syncDistributedMode.bind(this));
|
||||
this.serverConfigResource.onDataUpdate.addHandler(this.syncSettingsOverride.bind(this));
|
||||
}
|
||||
|
||||
switch() {
|
||||
this.settings.enabled = !this.settings.enabled;
|
||||
this.syncDistributedMode();
|
||||
this.syncSettingsOverride();
|
||||
}
|
||||
|
||||
setDistributedMode(distributed: boolean) {
|
||||
this.settings.distributed = distributed;
|
||||
this.syncDistributedMode();
|
||||
this.syncSettingsOverride();
|
||||
}
|
||||
|
||||
private syncDistributedMode() {
|
||||
setConfigurationMode(configuration: boolean) {
|
||||
this.settings.configuration = configuration;
|
||||
this.syncSettingsOverride();
|
||||
}
|
||||
|
||||
private syncSettingsOverride() {
|
||||
if (this.isEnabled) {
|
||||
if (this.serverConfigResource.data) {
|
||||
this.serverConfigResource.data.distributed = this.isDistributed;
|
||||
this.serverConfigResource.data.configurationMode = this.isConfiguration;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,7 +72,8 @@ export class DevToolsService {
|
||||
|
||||
function getDefaultDevToolsSettings(): IDevToolsSettings {
|
||||
return {
|
||||
enabled: false,
|
||||
enabled: process.env.NODE_ENV === 'development',
|
||||
distributed: false,
|
||||
configuration: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { TOP_NAV_BAR_SETTINGS_MENU } from '@cloudbeaver/plugin-settings-menu';
|
||||
import { MENU_USER_PROFILE } from '@cloudbeaver/plugin-user-profile';
|
||||
|
||||
import { ACTION_DEVTOOLS } from './actions/ACTION_DEVTOOLS';
|
||||
import { ACTION_DEVTOOLS_MODE_CONFIGURATION } from './actions/ACTION_DEVTOOLS_MODE_CONFIGURATION';
|
||||
import { ACTION_DEVTOOLS_MODE_DISTRIBUTED } from './actions/ACTION_DEVTOOLS_MODE_DISTRIBUTED';
|
||||
import { DATA_CONTEXT_MENU_SEARCH } from './ContextMenu/DATA_CONTEXT_MENU_SEARCH';
|
||||
import { SearchResourceMenuItem } from './ContextMenu/SearchResourceMenuItem';
|
||||
@@ -97,7 +98,16 @@ export class PluginBootstrap extends Bootstrap {
|
||||
];
|
||||
}
|
||||
|
||||
return [new SearchResourceMenuItem(), ACTION_DEVTOOLS_MODE_DISTRIBUTED, MENU_PLUGINS, ...items];
|
||||
return [new SearchResourceMenuItem(), ACTION_DEVTOOLS_MODE_DISTRIBUTED, ACTION_DEVTOOLS_MODE_CONFIGURATION, MENU_PLUGINS, ...items];
|
||||
},
|
||||
});
|
||||
|
||||
this.actionService.addHandler({
|
||||
id: 'devtools-mode-configuration',
|
||||
isActionApplicable: (context, action) => action === ACTION_DEVTOOLS_MODE_CONFIGURATION,
|
||||
isChecked: () => this.devToolsService.isConfiguration,
|
||||
handler: () => {
|
||||
this.devToolsService.setConfigurationMode(!this.devToolsService.isConfiguration);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2022 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 { createAction } from '@cloudbeaver/core-view';
|
||||
|
||||
export const ACTION_DEVTOOLS_MODE_CONFIGURATION = createAction('devtools-mode-configuration', {
|
||||
type: 'checkbox',
|
||||
label: 'Easy config mode',
|
||||
tooltip: 'Enable easy config mode',
|
||||
});
|
||||
Reference in New Issue
Block a user