Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ac2d73935 | ||
|
|
dcbf3f4d6d | ||
|
|
861c2eeb3d | ||
|
|
ad3b95f2a7 | ||
|
|
82775f9dc2 | ||
|
|
f95bb8c041 | ||
|
|
134a77e5bd |
@@ -43,6 +43,24 @@ jobs:
|
||||
uses: ./.github/workflows/frontend-lint.yml
|
||||
secrets: inherit
|
||||
|
||||
required-builds:
|
||||
name: Required Builds
|
||||
if: ${{ always() && github.event.pull_request.draft != true }}
|
||||
needs: [build-server, build-frontend]
|
||||
runs-on: ubuntu-latest
|
||||
permissions: {}
|
||||
steps:
|
||||
- name: Verify required builds
|
||||
env:
|
||||
SERVER_RESULT: ${{ needs.build-server.result }}
|
||||
FRONTEND_RESULT: ${{ needs.build-frontend.result }}
|
||||
run: |
|
||||
if [[ "$SERVER_RESULT" != "success" || "$FRONTEND_RESULT" != "success" ]]; then
|
||||
echo "Server build: $SERVER_RESULT"
|
||||
echo "Frontend build: $FRONTEND_RESULT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# call-frontend-tests:
|
||||
# name: Frontend Unit Tests
|
||||
# needs: call-frontend-build
|
||||
|
||||
@@ -83,6 +83,7 @@ For a focused check, use a package's existing script, for example `yarn workspac
|
||||
- Before backend work, read and follow `../dbeaver/AGENTS.md` for inherited Java and Tycho conventions.
|
||||
- Cloudbeaver does not use SWT or Eclipse RCP, and its backend is headless.
|
||||
- Configuration file is generated using `apps/config-generator`. For making changes there, update `config/template/cloudbeaver-base.conf` or use patches (for specific product parameters).
|
||||
- The backend must support at least the following databases: PostgreSQL, MySQL, Oracle, and SQL Server. Support for additional databases is optional.
|
||||
|
||||
### GraphQL API
|
||||
|
||||
|
||||
@@ -8,16 +8,15 @@ Bundle-Release-Date: 20260921
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Require-Bundle: org.jkiss.dbeaver.data.gis;visibility:=reexport,
|
||||
org.jkiss.dbeaver.model;visibility:=reexport,
|
||||
org.jkiss.dbeaver.model.sm;visibility:=reexport,
|
||||
org.jkiss.dbeaver.model.event;visibility:=reexport,
|
||||
org.jkiss.dbeaver.model.nio;visibility:=reexport,
|
||||
org.jkiss.dbeaver.registry;visibility:=reexport,
|
||||
org.jkiss.dbeaver.net.ssh,
|
||||
org.jkiss.dbeaver.net.ssh;visibility:=reexport,
|
||||
org.jkiss.bundle.logback;visibility:=reexport,
|
||||
org.jkiss.bundle.graphql;visibility:=reexport,
|
||||
org.jkiss.bundle.apache.dbcp,
|
||||
jakarta.servlet-api;bundle-version:="6.0.0";visibility:=reexport
|
||||
jakarta.servlet-api;visibility:=reexport
|
||||
Export-Package: io.cloudbeaver,
|
||||
io.cloudbeaver.auth,
|
||||
io.cloudbeaver.auth.provider,
|
||||
|
||||
@@ -7,12 +7,7 @@ Bundle-Version: 26.2.1.qualifier
|
||||
Bundle-Release-Date: 20260921
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Require-Bundle: org.jkiss.dbeaver.model.sql;visibility:=reexport,
|
||||
org.jkiss.dbeaver.data.gis,
|
||||
org.jkiss.bundle.jetty.server;visibility:=reexport,
|
||||
org.jkiss.bundle.graphql;visibility:=reexport,
|
||||
org.jkiss.bundle.apache.dbcp,
|
||||
org.jkiss.dbeaver.net.ssh,
|
||||
Require-Bundle: org.jkiss.bundle.jetty.server;visibility:=reexport,
|
||||
io.cloudbeaver.model;visibility:=reexport,
|
||||
io.cloudbeaver.model.cli,
|
||||
io.cloudbeaver.service.security;visibility:=reexport
|
||||
|
||||
@@ -7,6 +7,5 @@ Bundle-Version: 1.0.153.qualifier
|
||||
Bundle-Release-Date: 20260921
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Require-Bundle: io.cloudbeaver.server.ce,
|
||||
org.jkiss.dbeaver.registry
|
||||
Require-Bundle: io.cloudbeaver.server.ce
|
||||
Automatic-Module-Name: io.cloudbeaver.service.admin
|
||||
|
||||
@@ -7,9 +7,8 @@ Bundle-Release-Date: 20260921
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Automatic-Module-Name: io.cloudbeaver.service.ai
|
||||
Require-Bundle: org.jkiss.dbeaver.model.ai,
|
||||
io.cloudbeaver.model,
|
||||
io.cloudbeaver.server.ce
|
||||
Require-Bundle: org.jkiss.dbeaver.model.ai;visibility:=reexport,
|
||||
io.cloudbeaver.server.ce;visibility:=reexport
|
||||
Export-Package: io.cloudbeaver.service.ai,
|
||||
io.cloudbeaver.service.ai.model,
|
||||
io.cloudbeaver.service.ai.model.inputs
|
||||
|
||||
@@ -8,6 +8,5 @@ Bundle-Release-Date: 20260921
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Require-Bundle: io.cloudbeaver.server.ce,
|
||||
org.jkiss.dbeaver.data.transfer,
|
||||
org.jkiss.dbeaver.registry
|
||||
org.jkiss.dbeaver.data.transfer
|
||||
Automatic-Module-Name: io.cloudbeaver.service.data.transfer
|
||||
|
||||
@@ -7,9 +7,7 @@ Bundle-Version: 1.0.36.qualifier
|
||||
Bundle-Release-Date: 20260921
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Require-Bundle: org.jkiss.dbeaver.model;visibility:=reexport,
|
||||
org.jkiss.dbeaver.registry;visibility:=reexport,
|
||||
io.cloudbeaver.model
|
||||
Require-Bundle: io.cloudbeaver.model;visibility:=reexport
|
||||
Bundle-Localization: OSGI-INF/l10n/bundle
|
||||
Export-Package: io.cloudbeaver.service.ldap.auth,
|
||||
io.cloudbeaver.service.ldap.auth.ssl
|
||||
|
||||
@@ -7,9 +7,7 @@ Bundle-Version: 1.0.71.qualifier
|
||||
Bundle-Release-Date: 20260921
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Require-Bundle: io.cloudbeaver.model,
|
||||
org.jkiss.dbeaver.model,
|
||||
org.jkiss.dbeaver.model.nio
|
||||
Require-Bundle: io.cloudbeaver.model;visibility:=reexport
|
||||
Export-Package: io.cloudbeaver.service.rm.nio,
|
||||
io.cloudbeaver.service.rm.fs
|
||||
Automatic-Module-Name: io.cloudbeaver.service.rm.nio
|
||||
|
||||
@@ -7,13 +7,9 @@ Bundle-Version: 1.0.109.qualifier
|
||||
Bundle-Release-Date: 20260921
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Require-Bundle: org.jkiss.dbeaver.model;visibility:=reexport,
|
||||
org.jkiss.dbeaver.model.jdbc,
|
||||
org.jkiss.dbeaver.model.sql,
|
||||
org.jkiss.dbeaver.model.sql.jdbc;visibility:=reexport,
|
||||
org.jkiss.dbeaver.registry;visibility:=reexport,
|
||||
Require-Bundle: org.jkiss.dbeaver.model.sql.jdbc;visibility:=reexport,
|
||||
org.jkiss.bundle.apache.dbcp;visibility:=reexport,
|
||||
io.cloudbeaver.model
|
||||
io.cloudbeaver.model;visibility:=reexport
|
||||
Export-Package: io.cloudbeaver.auth.provider.local,
|
||||
io.cloudbeaver.auth.provider.rp,
|
||||
io.cloudbeaver.service.security,
|
||||
|
||||
@@ -81,6 +81,8 @@ public class CBEmbeddedSecurityController<T extends ServletAuthApplication>
|
||||
|
||||
protected static final String CHAR_BOOL_TRUE = "Y";
|
||||
protected static final String CHAR_BOOL_FALSE = "N";
|
||||
private static final String CRED_ID_PROVISIONED = "$provisioned";
|
||||
private static final String CRED_VALUE_PROVISIONED = "Y";
|
||||
|
||||
private static final Type MAP_STRING_OBJECT_TYPE = new TypeToken<Map<String, Object>>() {
|
||||
}.getType();
|
||||
@@ -419,13 +421,8 @@ public class CBEmbeddedSecurityController<T extends ServletAuthApplication>
|
||||
List<SMUserProvisioning> users = userImportList.getUsers();
|
||||
List<SMUserProvisioning> importedUsers = new ArrayList<>();
|
||||
Set<String> seen = new HashSet<>();
|
||||
outer:
|
||||
for (SMUserProvisioning user : users) {
|
||||
Map<String, String> metaParameters = user.getMetaParameters();
|
||||
String effectiveUserId = user.getUserId();
|
||||
if (CommonUtils.isNotEmpty(metaParameters.get(SMStandardMeta.META_USER_ID))) {
|
||||
effectiveUserId = metaParameters.get(SMStandardMeta.META_USER_ID);
|
||||
}
|
||||
String effectiveUserId = extractUserId(user);
|
||||
String effectiveUserIdLowerCase = effectiveUserId.toLowerCase();
|
||||
if (seen.contains(effectiveUserIdLowerCase)) {
|
||||
log.warn("Skipping duplicate user (case-insensitive): " + effectiveUserId);
|
||||
@@ -434,25 +431,109 @@ public class CBEmbeddedSecurityController<T extends ServletAuthApplication>
|
||||
seen.add(effectiveUserIdLowerCase);
|
||||
|
||||
String authRole = user.getAuthRole() == null ? userImportList.getAuthRole() : user.getAuthRole();
|
||||
String userId = effectiveUserId;
|
||||
for (String possibleUserId : List.of(userId, userId.toLowerCase())) {
|
||||
String targetUserId = null;
|
||||
boolean subjectConflict = false;
|
||||
for (String possibleUserId : List.of(effectiveUserId, effectiveUserId.toLowerCase())) {
|
||||
if (isSubjectExists(possibleUserId)) {
|
||||
if (getSubjectType(possibleUserId) == SMSubjectType.team) {
|
||||
log.error("Cannot import user '%s': a team with this name already exists.".formatted(possibleUserId));
|
||||
subjectConflict = true;
|
||||
} else {
|
||||
log.info("User already exist : " + possibleUserId);
|
||||
setUserAuthRole(connection, possibleUserId, authRole);
|
||||
enableUser(connection, possibleUserId, true, null, null);
|
||||
targetUserId = possibleUserId;
|
||||
}
|
||||
continue outer;
|
||||
break;
|
||||
}
|
||||
}
|
||||
insertUser(connection, userId.toLowerCase(), metaParameters, true, authRole);
|
||||
importedUsers.add(user);
|
||||
if (subjectConflict) {
|
||||
continue;
|
||||
}
|
||||
if (targetUserId == null) {
|
||||
targetUserId = effectiveUserId.toLowerCase();
|
||||
insertUser(connection, targetUserId, user.getMetaParameters(), true, authRole);
|
||||
importedUsers.add(user);
|
||||
}
|
||||
if (CommonUtils.isNotEmpty(userImportList.getAuthProviderId())) {
|
||||
linkProvisionedAuthProvider(connection, targetUserId, userImportList.getAuthProviderId());
|
||||
}
|
||||
}
|
||||
return importedUsers;
|
||||
}
|
||||
|
||||
protected void linkProvisionedAuthProvider(
|
||||
@NotNull Connection connection,
|
||||
@NotNull String userId,
|
||||
@NotNull String authProviderId
|
||||
) throws SQLException {
|
||||
try (PreparedStatement dbStat = connection.prepareStatement(
|
||||
"SELECT USER_ID FROM {table_prefix}CB_USER WHERE USER_ID=?"
|
||||
)) {
|
||||
dbStat.setString(1, userId);
|
||||
try (ResultSet dbResult = dbStat.executeQuery()) {
|
||||
if (!dbResult.next()) {
|
||||
throw new SQLException("User '" + userId + "' not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
try (PreparedStatement dbStat = connection.prepareStatement(
|
||||
"SELECT CRED_ID FROM {table_prefix}CB_USER_CREDENTIALS WHERE USER_ID=? AND PROVIDER_ID=?"
|
||||
)) {
|
||||
dbStat.setString(1, userId);
|
||||
dbStat.setString(2, authProviderId);
|
||||
try (ResultSet dbResult = dbStat.executeQuery()) {
|
||||
if (dbResult.next()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
JDBCUtils.executeStatement(
|
||||
connection,
|
||||
"INSERT INTO {table_prefix}CB_USER_CREDENTIALS(USER_ID,PROVIDER_ID,CRED_ID,CRED_VALUE) VALUES(?,?,?,?)",
|
||||
userId,
|
||||
authProviderId,
|
||||
CRED_ID_PROVISIONED,
|
||||
CRED_VALUE_PROVISIONED
|
||||
);
|
||||
}
|
||||
|
||||
protected void linkProvisionedAuthProviders(@NotNull SMUserImportList userImportList) throws DBException {
|
||||
String authProviderId = userImportList.getAuthProviderId();
|
||||
if (CommonUtils.isEmpty(authProviderId)) {
|
||||
return;
|
||||
}
|
||||
try (Connection connection = database.openConnection();
|
||||
JDBCTransaction transaction = new JDBCTransaction(connection)) {
|
||||
for (SMUserProvisioning user : userImportList.getUsers()) {
|
||||
String userId = extractUserId(user);
|
||||
String existingUserId = null;
|
||||
for (String possibleUserId : List.of(userId, userId.toLowerCase())) {
|
||||
if (isSubjectExists(possibleUserId) && getSubjectType(possibleUserId) == SMSubjectType.user) {
|
||||
existingUserId = possibleUserId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (existingUserId == null) {
|
||||
throw new DBException("Imported user '" + userId + "' not found");
|
||||
}
|
||||
linkProvisionedAuthProvider(connection, existingUserId, authProviderId);
|
||||
}
|
||||
transaction.commit();
|
||||
} catch (SQLException e) {
|
||||
throw new DBCException("Error linking provisioned auth provider", e);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String extractUserId(SMUserProvisioning user) {
|
||||
String metaUserId = user.getMetaParameters().get(SMStandardMeta.META_USER_ID);
|
||||
if (CommonUtils.isNotEmpty(metaUserId)) {
|
||||
return metaUserId;
|
||||
}
|
||||
return user.getUserId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteUser(String userId) throws DBCException {
|
||||
invalidateAllUserTokens(userId);
|
||||
@@ -1173,12 +1254,28 @@ public class CBEmbeddedSecurityController<T extends ServletAuthApplication>
|
||||
@NotNull String authProviderId,
|
||||
@NotNull Map<String, Object> credentials
|
||||
) throws DBException {
|
||||
var existUserByCredentials = findUserByCredentials(getAuthProvider(authProviderId), credentials, false);
|
||||
try (Connection dbCon = database.openConnection()) {
|
||||
try (JDBCTransaction txn = new JDBCTransaction(dbCon)) {
|
||||
setUserCredentials(dbCon, userId, authProviderId, credentials);
|
||||
txn.commit();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new DBCException("Error saving user credentials in database", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void setUserCredentials(
|
||||
@NotNull Connection dbCon,
|
||||
@NotNull String userId,
|
||||
@NotNull String authProviderId,
|
||||
@NotNull Map<String, Object> credentials
|
||||
) throws DBException, SQLException {
|
||||
WebAuthProviderDescriptor authProvider = getAuthProvider(authProviderId);
|
||||
var existUserByCredentials = findUserByCredentials(dbCon, authProvider, credentials, false);
|
||||
if (existUserByCredentials != null && !existUserByCredentials.equals(userId)) {
|
||||
throw new DBException("Another user is already linked to the specified credentials");
|
||||
}
|
||||
List<String[]> transformedCredentials;
|
||||
WebAuthProviderDescriptor authProvider = getAuthProvider(authProviderId);
|
||||
if (authProvider.isCaseInsensitive() && !isSubjectExists(userId) && isSubjectExists(userId.toLowerCase())) {
|
||||
log.warn("User with id '" + userId + "' not found, credentials will be set for the user: " + userId.toLowerCase());
|
||||
userId = userId.toLowerCase();
|
||||
@@ -1199,35 +1296,28 @@ public class CBEmbeddedSecurityController<T extends ServletAuthApplication>
|
||||
} catch (Exception e) {
|
||||
throw new DBCException(e.getMessage(), e);
|
||||
}
|
||||
try (Connection dbCon = database.openConnection()) {
|
||||
try (JDBCTransaction txn = new JDBCTransaction(dbCon)) {
|
||||
JDBCUtils.executeStatement(
|
||||
dbCon,
|
||||
"DELETE FROM {table_prefix}CB_USER_CREDENTIALS WHERE USER_ID=? AND PROVIDER_ID=?",
|
||||
userId,
|
||||
authProvider.getId()
|
||||
);
|
||||
if (!CommonUtils.isEmpty(credentials)) {
|
||||
try (PreparedStatement dbStat = dbCon.prepareStatement(
|
||||
"INSERT INTO {table_prefix}CB_USER_CREDENTIALS" +
|
||||
"(USER_ID,PROVIDER_ID,CRED_ID,CRED_VALUE) VALUES(?,?,?,?)")
|
||||
) {
|
||||
for (String[] cred : transformedCredentials) {
|
||||
if (cred == null) {
|
||||
continue;
|
||||
}
|
||||
dbStat.setString(1, userId);
|
||||
dbStat.setString(2, authProvider.getId());
|
||||
dbStat.setString(3, cred[0]);
|
||||
dbStat.setString(4, cred[1]);
|
||||
dbStat.execute();
|
||||
}
|
||||
JDBCUtils.executeStatement(
|
||||
dbCon,
|
||||
"DELETE FROM {table_prefix}CB_USER_CREDENTIALS WHERE USER_ID=? AND PROVIDER_ID=?",
|
||||
userId,
|
||||
authProvider.getId()
|
||||
);
|
||||
if (!CommonUtils.isEmpty(credentials)) {
|
||||
try (PreparedStatement dbStat = dbCon.prepareStatement(
|
||||
"INSERT INTO {table_prefix}CB_USER_CREDENTIALS" +
|
||||
"(USER_ID,PROVIDER_ID,CRED_ID,CRED_VALUE) VALUES(?,?,?,?)")
|
||||
) {
|
||||
for (String[] cred : transformedCredentials) {
|
||||
if (cred == null) {
|
||||
continue;
|
||||
}
|
||||
dbStat.setString(1, userId);
|
||||
dbStat.setString(2, authProvider.getId());
|
||||
dbStat.setString(3, cred[0]);
|
||||
dbStat.setString(4, cred[1]);
|
||||
dbStat.execute();
|
||||
}
|
||||
txn.commit();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new DBCException("Error saving user credentials in database", e);
|
||||
}
|
||||
log.info(String.format("Set credentials for user: [userId=%s,providerId=%s]", userId, authProviderId));
|
||||
}
|
||||
@@ -1253,16 +1343,31 @@ public class CBEmbeddedSecurityController<T extends ServletAuthApplication>
|
||||
@NotNull Map<String, Object> authParameters,
|
||||
boolean onlyActive // throws exception if user is inactive
|
||||
) throws DBException {
|
||||
String userId = findUserByCredentials(authProvider, authParameters, onlyActive, false);
|
||||
try (Connection dbCon = database.openConnection()) {
|
||||
return findUserByCredentials(dbCon, authProvider, authParameters, onlyActive);
|
||||
} catch (SQLException e) {
|
||||
throw new DBCException("Error while searching credentials", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String findUserByCredentials(
|
||||
@NotNull Connection dbCon,
|
||||
@NotNull WebAuthProviderDescriptor authProvider,
|
||||
@NotNull Map<String, Object> authParameters,
|
||||
boolean onlyActive
|
||||
) throws DBCException {
|
||||
String userId = findUserByCredentials(dbCon, authProvider, authParameters, onlyActive, false);
|
||||
if (userId == null && authProvider.isCaseInsensitive()) {
|
||||
// try to find user id with lower case is auth provider is case-insensitive
|
||||
return findUserByCredentials(authProvider, authParameters, onlyActive, true);
|
||||
return findUserByCredentials(dbCon, authProvider, authParameters, onlyActive, true);
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String findUserByCredentials(
|
||||
@NotNull Connection dbCon,
|
||||
@NotNull WebAuthProviderDescriptor authProvider,
|
||||
@NotNull Map<String, Object> authParameters,
|
||||
boolean onlyActive,
|
||||
@@ -1303,34 +1408,32 @@ public class CBEmbeddedSecurityController<T extends ServletAuthApplication>
|
||||
.append(joinAlias).append("CRED_ID=? AND ")
|
||||
.append(joinAlias).append("CRED_VALUE=?");
|
||||
}
|
||||
try (Connection dbCon = database.openConnection()) {
|
||||
try (PreparedStatement dbStat = dbCon.prepareStatement(sql.toString())) {
|
||||
dbStat.setString(1, authProvider.getId());
|
||||
int param = 2;
|
||||
for (Map.Entry<String, String> credEntry : identCredentials.entrySet()) {
|
||||
dbStat.setString(param++, credEntry.getKey());
|
||||
dbStat.setString(param++, credEntry.getValue());
|
||||
try (PreparedStatement dbStat = dbCon.prepareStatement(sql.toString())) {
|
||||
dbStat.setString(1, authProvider.getId());
|
||||
int param = 2;
|
||||
for (Map.Entry<String, String> credEntry : identCredentials.entrySet()) {
|
||||
dbStat.setString(param++, credEntry.getKey());
|
||||
dbStat.setString(param++, credEntry.getValue());
|
||||
}
|
||||
|
||||
try (ResultSet dbResult = dbStat.executeQuery()) {
|
||||
String userId = null;
|
||||
boolean isActive = false;
|
||||
while (dbResult.next()) {
|
||||
String credUserId = dbResult.getString(1);
|
||||
isActive = CHAR_BOOL_TRUE.equals(dbResult.getString(2));
|
||||
if (userId == null) {
|
||||
userId = credUserId;
|
||||
} else if (!userId.equals(credUserId)) {
|
||||
log.error("Multiple users associated with the same credentials! " + credUserId + ", " + userId);
|
||||
}
|
||||
}
|
||||
|
||||
try (ResultSet dbResult = dbStat.executeQuery()) {
|
||||
String userId = null;
|
||||
boolean isActive = false;
|
||||
while (dbResult.next()) {
|
||||
String credUserId = dbResult.getString(1);
|
||||
isActive = CHAR_BOOL_TRUE.equals(dbResult.getString(2));
|
||||
if (userId == null) {
|
||||
userId = credUserId;
|
||||
} else if (!userId.equals(credUserId)) {
|
||||
log.error("Multiple users associated with the same credentials! " + credUserId + ", " + userId);
|
||||
}
|
||||
}
|
||||
|
||||
if (userId != null && onlyActive && !isActive) {
|
||||
throw new DBCException("User account is locked");
|
||||
}
|
||||
|
||||
return userId;
|
||||
if (userId != null && onlyActive && !isActive) {
|
||||
throw new DBCException("User account is locked");
|
||||
}
|
||||
|
||||
return userId;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new DBCException("Error while searching credentials", e);
|
||||
@@ -1341,7 +1444,7 @@ public class CBEmbeddedSecurityController<T extends ServletAuthApplication>
|
||||
public Map<String, Object> getUserCredentials(String userId, String authProviderId) throws DBCException {
|
||||
WebAuthProviderDescriptor authProvider = getAuthProvider(authProviderId);
|
||||
Map<String, Object> creds = getUserCredentials(authProvider, userId);
|
||||
if (creds.isEmpty() && authProvider.isCaseInsensitive()) {
|
||||
if (creds.isEmpty() && authProvider.isCaseInsensitive() && !isSubjectExists(userId)) {
|
||||
return getUserCredentials(authProvider, userId.toLowerCase());
|
||||
}
|
||||
return creds;
|
||||
@@ -1352,10 +1455,11 @@ public class CBEmbeddedSecurityController<T extends ServletAuthApplication>
|
||||
try (Connection dbCon = database.openConnection()) {
|
||||
try (PreparedStatement dbStat = dbCon.prepareStatement(
|
||||
"SELECT CRED_ID,CRED_VALUE FROM {table_prefix}CB_USER_CREDENTIALS\n" +
|
||||
"WHERE USER_ID=? AND PROVIDER_ID=?")) {
|
||||
"WHERE USER_ID=? AND PROVIDER_ID=? AND CRED_ID<>?")) {
|
||||
dbStat.setString(1, userId);
|
||||
|
||||
dbStat.setString(2, authProvider.getId());
|
||||
dbStat.setString(3, CRED_ID_PROVISIONED);
|
||||
|
||||
try (ResultSet dbResult = dbStat.executeQuery()) {
|
||||
Map<String, Object> credentials = new LinkedHashMap<>();
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<dependency>
|
||||
<groupId>com.clickhouse</groupId>
|
||||
<artifactId>clickhouse-jdbc</artifactId>
|
||||
<version>0.10.0-rc1</version>
|
||||
<version>0.10.0</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
|
||||
@@ -27,11 +27,8 @@ Require-Bundle: org.eclipse.core.runtime,
|
||||
io.cloudbeaver.service.rm,
|
||||
io.cloudbeaver.service.rm.nio,
|
||||
org.jkiss.dbeaver.ext.mysql,
|
||||
org.jkiss.dbeaver.ext.postgresql,
|
||||
org.jkiss.dbeaver.ext.oracle,
|
||||
org.jkiss.dbeaver.ext.mssql,
|
||||
org.jkiss.dbeaver.ext.h2,
|
||||
org.jkiss.dbeaver.ext.generic,
|
||||
org.eclipse.lsp4j,
|
||||
org.eclipse.lsp4j.jsonrpc
|
||||
Export-Package: io.cloudbeaver.test.platform,
|
||||
|
||||
@@ -24,6 +24,7 @@ import io.cloudbeaver.model.rm.lock.RMLockTest;
|
||||
import io.cloudbeaver.model.session.WebSessionProjectTest;
|
||||
import io.cloudbeaver.model.session.WebSessionTest;
|
||||
import io.cloudbeaver.test.platform.admin.AdminCreateUserTest;
|
||||
import io.cloudbeaver.test.platform.admin.AdminImportUsersTest;
|
||||
import io.cloudbeaver.test.platform.admin.AdminLastLoginTimeTest;
|
||||
import io.cloudbeaver.test.platform.sql.*;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
@@ -45,6 +46,7 @@ import org.junit.platform.suite.api.Suite;
|
||||
WebSessionProjectTest.class,
|
||||
WebNavigatorNodeInfoTest.class,
|
||||
AdminCreateUserTest.class,
|
||||
AdminImportUsersTest.class,
|
||||
AdminLastLoginTimeTest.class,
|
||||
GenerateSQLResultSetTest.class,
|
||||
RowIdResultSetTest.class,
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.cloudbeaver.test.platform.admin;
|
||||
|
||||
import io.cloudbeaver.CloudbeaverMockTest;
|
||||
import io.cloudbeaver.app.CEAppStarter;
|
||||
import io.cloudbeaver.auth.provider.local.LocalAuthProvider;
|
||||
import org.jkiss.dbeaver.model.security.SMAdminController;
|
||||
import org.jkiss.dbeaver.model.security.user.SMUserImportList;
|
||||
import org.jkiss.dbeaver.model.security.user.SMUserProvisioning;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public class AdminImportUsersTest extends CloudbeaverMockTest {
|
||||
@Test
|
||||
public void importMaintainsProvisionedAuthProviderLifecycle() throws Exception {
|
||||
SMAdminController securityController = CEAppStarter.getTestApp().getSecurityController();
|
||||
String userId = "import-user-" + UUID.randomUUID();
|
||||
var user = new SMUserProvisioning(
|
||||
userId,
|
||||
Map.of(),
|
||||
null
|
||||
);
|
||||
var importList = new SMUserImportList(
|
||||
List.of(user), null, LocalAuthProvider.PROVIDER_ID);
|
||||
|
||||
try {
|
||||
Assertions.assertEquals(List.of(user), securityController.importUsers(importList));
|
||||
Assertions.assertArrayEquals(
|
||||
new String[]{LocalAuthProvider.PROVIDER_ID},
|
||||
securityController.getUserLinkedProviders(userId)
|
||||
);
|
||||
Assertions.assertTrue(
|
||||
securityController.getUserCredentials(userId, LocalAuthProvider.PROVIDER_ID).isEmpty()
|
||||
);
|
||||
|
||||
Assertions.assertTrue(securityController.importUsers(importList).isEmpty());
|
||||
Assertions.assertArrayEquals(
|
||||
new String[]{LocalAuthProvider.PROVIDER_ID},
|
||||
securityController.getUserLinkedProviders(userId)
|
||||
);
|
||||
Assertions.assertTrue(
|
||||
securityController.getUserCredentials(userId, LocalAuthProvider.PROVIDER_ID).isEmpty()
|
||||
);
|
||||
|
||||
securityController.deleteUserCredentials(userId, LocalAuthProvider.PROVIDER_ID);
|
||||
Assertions.assertArrayEquals(new String[0], securityController.getUserLinkedProviders(userId));
|
||||
|
||||
Assertions.assertTrue(securityController.importUsers(importList).isEmpty());
|
||||
Assertions.assertArrayEquals(
|
||||
new String[]{LocalAuthProvider.PROVIDER_ID},
|
||||
securityController.getUserLinkedProviders(userId)
|
||||
);
|
||||
|
||||
securityController.setUserCredentials(
|
||||
userId,
|
||||
LocalAuthProvider.PROVIDER_ID,
|
||||
Map.of(
|
||||
LocalAuthProvider.CRED_USER, userId,
|
||||
LocalAuthProvider.CRED_PASSWORD, "password"
|
||||
)
|
||||
);
|
||||
Assertions.assertEquals(
|
||||
userId,
|
||||
securityController.getUserCredentials(userId, LocalAuthProvider.PROVIDER_ID)
|
||||
.get(LocalAuthProvider.CRED_USER)
|
||||
);
|
||||
|
||||
Assertions.assertTrue(securityController.importUsers(importList).isEmpty());
|
||||
Assertions.assertEquals(
|
||||
userId,
|
||||
securityController.getUserCredentials(userId, LocalAuthProvider.PROVIDER_ID)
|
||||
.get(LocalAuthProvider.CRED_USER)
|
||||
);
|
||||
|
||||
securityController.deleteUserCredentials(userId, LocalAuthProvider.PROVIDER_ID);
|
||||
Assertions.assertArrayEquals(new String[0], securityController.getUserLinkedProviders(userId));
|
||||
} finally {
|
||||
securityController.deleteUser(userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 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.
|
||||
@@ -20,6 +20,7 @@ export class FormBaseService<TState, TProps extends IFormProps<TState> = IFormPr
|
||||
readonly parts: TabsContainer<TProps>;
|
||||
readonly actionsContainer: PlaceholderContainer<TProps>;
|
||||
|
||||
readonly onPrepare: IExecutorHandlersCollection<IFormState<TState>>;
|
||||
readonly onFormat: IExecutorHandlersCollection<IFormState<TState>>;
|
||||
readonly onValidate: IExecutorHandlersCollection<IFormState<TState>>;
|
||||
readonly onSubmit: IExecutorHandlersCollection<IFormState<TState>>;
|
||||
@@ -33,6 +34,7 @@ export class FormBaseService<TState, TProps extends IFormProps<TState> = IFormPr
|
||||
) {
|
||||
this.parts = new TabsContainer(name);
|
||||
this.actionsContainer = new PlaceholderContainer();
|
||||
this.onPrepare = new ExecutorHandlersCollection();
|
||||
this.onFormat = new ExecutorHandlersCollection();
|
||||
this.onValidate = new ExecutorHandlersCollection();
|
||||
this.onSubmit = new ExecutorHandlersCollection();
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* 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 { describe, expect, it } from 'vitest';
|
||||
|
||||
import { Executor, ExecutorInterrupter, type IExecutionContextProvider, type IExecutor } from '@cloudbeaver/core-executor';
|
||||
import { schema } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { FormPart } from './FormPart.js';
|
||||
import type { IFormState } from './IFormState.js';
|
||||
import { formValidationContext } from './formValidationContext.js';
|
||||
|
||||
interface TestFormPartOptions {
|
||||
loader?: () => Promise<void>;
|
||||
loaderError?: Error;
|
||||
schema?: schema.ZodType<{ value: string }>;
|
||||
}
|
||||
|
||||
class TestFormState {
|
||||
readonly loadedTask: IExecutor<IFormState<null>>;
|
||||
readonly submitTask: IExecutor<IFormState<null>>;
|
||||
readonly prepareTask: IExecutor<IFormState<null>>;
|
||||
readonly formatTask: IExecutor<IFormState<null>>;
|
||||
readonly validationTask: IExecutor<IFormState<null>>;
|
||||
savingPromise: Promise<IExecutionContextProvider<IFormState<null>>> | null;
|
||||
|
||||
constructor() {
|
||||
const data = this.asFormState();
|
||||
|
||||
this.savingPromise = null;
|
||||
this.loadedTask = new Executor(data, () => true);
|
||||
this.prepareTask = new Executor(data, () => true);
|
||||
this.formatTask = new Executor(data, () => true);
|
||||
this.formatTask.before(this.prepareTask);
|
||||
|
||||
this.validationTask = new Executor(data, () => true);
|
||||
this.validationTask.before(this.formatTask);
|
||||
this.validationTask.addPostHandler((formState, contexts) => {
|
||||
if (!contexts.getContext(formValidationContext).valid) {
|
||||
ExecutorInterrupter.interrupt(contexts);
|
||||
}
|
||||
});
|
||||
|
||||
this.submitTask = new Executor(data, () => true);
|
||||
this.submitTask.before(this.validationTask);
|
||||
}
|
||||
|
||||
async save(): Promise<boolean> {
|
||||
const data = this.asFormState();
|
||||
|
||||
try {
|
||||
this.savingPromise = this.submitTask.execute(data);
|
||||
const context = await this.savingPromise;
|
||||
|
||||
return !ExecutorInterrupter.isInterrupted(context);
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
this.savingPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
asFormState(): IFormState<null> {
|
||||
return this as unknown as IFormState<null>;
|
||||
}
|
||||
}
|
||||
|
||||
class TestFormPart extends FormPart<{ value: string }> {
|
||||
constructor(
|
||||
formState: TestFormState,
|
||||
private readonly calls: string[],
|
||||
private readonly options: TestFormPartOptions = {},
|
||||
) {
|
||||
super(formState.asFormState(), { value: '' }, options.schema ?? null);
|
||||
}
|
||||
|
||||
protected override async loader(): Promise<void> {
|
||||
this.calls.push('load');
|
||||
|
||||
if (this.options.loaderError) {
|
||||
throw this.options.loaderError;
|
||||
}
|
||||
|
||||
await this.options.loader?.();
|
||||
}
|
||||
|
||||
protected override prepare(): void {
|
||||
this.calls.push('prepare');
|
||||
}
|
||||
|
||||
protected override format(): void {
|
||||
this.calls.push('format');
|
||||
}
|
||||
|
||||
protected override validate(): void {
|
||||
this.calls.push('validate');
|
||||
}
|
||||
|
||||
protected override saveChanges(): void {
|
||||
this.calls.push('save');
|
||||
}
|
||||
}
|
||||
|
||||
function createFormState(): TestFormState {
|
||||
return new TestFormState();
|
||||
}
|
||||
|
||||
describe('FormPart', () => {
|
||||
it('shares concurrent loading and records the loaded state', async () => {
|
||||
let finishLoading: (() => void) | undefined;
|
||||
const loading = new Promise<void>(resolve => {
|
||||
finishLoading = resolve;
|
||||
});
|
||||
const calls: string[] = [];
|
||||
const part = new TestFormPart(createFormState(), calls, { loader: () => loading });
|
||||
|
||||
const firstLoad = part.load();
|
||||
const secondLoad = part.load();
|
||||
|
||||
expect(part.isLoading()).toBe(true);
|
||||
expect(calls).toEqual(['load']);
|
||||
|
||||
finishLoading?.();
|
||||
await Promise.all([firstLoad, secondLoad]);
|
||||
|
||||
expect(part.isLoaded()).toBe(true);
|
||||
expect(part.isLoading()).toBe(false);
|
||||
expect(part.isError()).toBe(false);
|
||||
});
|
||||
|
||||
it('prepares loaded unchanged parts without formatting them', async () => {
|
||||
const calls: string[] = [];
|
||||
const formState = createFormState();
|
||||
const part = new TestFormPart(formState, calls);
|
||||
await part.load();
|
||||
calls.length = 0;
|
||||
|
||||
await formState.formatTask.execute(formState.asFormState());
|
||||
|
||||
expect(calls).toEqual(['prepare']);
|
||||
});
|
||||
|
||||
it('prepares before formatting and validating changed parts', async () => {
|
||||
const calls: string[] = [];
|
||||
const formState = createFormState();
|
||||
const part = new TestFormPart(formState, calls);
|
||||
await part.load();
|
||||
calls.length = 0;
|
||||
part.state.value = 'changed';
|
||||
|
||||
await formState.validationTask.execute(formState.asFormState());
|
||||
|
||||
expect(calls).toEqual(['prepare', 'format', 'validate']);
|
||||
});
|
||||
|
||||
it('runs the complete changed-part submission flow in order', async () => {
|
||||
const calls: string[] = [];
|
||||
const formState = createFormState();
|
||||
const part = new TestFormPart(formState, calls);
|
||||
await part.load();
|
||||
calls.length = 0;
|
||||
part.state.value = 'changed';
|
||||
|
||||
await expect(formState.save()).resolves.toBe(true);
|
||||
|
||||
expect(calls).toEqual(['prepare', 'format', 'validate', 'load', 'save']);
|
||||
expect(part.isLoaded()).toBe(false);
|
||||
expect(part.isSaving).toBe(false);
|
||||
expect(part.isLoading()).toBe(false);
|
||||
expect(part.isError()).toBe(false);
|
||||
});
|
||||
|
||||
it('prepares and validates unchanged parts without formatting or saving them', async () => {
|
||||
const calls: string[] = [];
|
||||
const formState = createFormState();
|
||||
const part = new TestFormPart(formState, calls);
|
||||
await part.load();
|
||||
calls.length = 0;
|
||||
|
||||
await expect(formState.save()).resolves.toBe(true);
|
||||
|
||||
expect(calls).toEqual(['prepare', 'validate', 'load']);
|
||||
expect(part.isLoaded()).toBe(true);
|
||||
});
|
||||
|
||||
it('stops submission when schema validation fails', async () => {
|
||||
const calls: string[] = [];
|
||||
const formState = createFormState();
|
||||
const part = new TestFormPart(formState, calls, {
|
||||
schema: schema.object({ value: schema.string().min(1) }),
|
||||
});
|
||||
await part.load();
|
||||
calls.length = 0;
|
||||
|
||||
const contexts = await formState.validationTask.execute(formState.asFormState());
|
||||
|
||||
expect(contexts.getContext(formValidationContext).valid).toBe(false);
|
||||
expect(calls).toEqual(['prepare']);
|
||||
await expect(formState.save()).resolves.toBe(false);
|
||||
expect(calls).toEqual(['prepare', 'prepare']);
|
||||
});
|
||||
|
||||
it('resets changes to the initial state', async () => {
|
||||
const part = new TestFormPart(createFormState(), []);
|
||||
await part.load();
|
||||
part.state.value = 'changed';
|
||||
|
||||
expect(part.isChanged).toBe(true);
|
||||
|
||||
part.reset();
|
||||
|
||||
expect(part.state).toEqual({ value: '' });
|
||||
expect(part.isChanged).toBe(false);
|
||||
});
|
||||
|
||||
it('records load errors and clears them after a successful reload', async () => {
|
||||
const error = new Error('load failed');
|
||||
const options: TestFormPartOptions = { loaderError: error };
|
||||
const part = new TestFormPart(createFormState(), [], options);
|
||||
|
||||
await part.load();
|
||||
|
||||
expect(part.exception).toBe(error);
|
||||
expect(part.isLoaded()).toBe(true);
|
||||
expect(part.isLoading()).toBe(false);
|
||||
|
||||
options.loaderError = undefined;
|
||||
await part.reload();
|
||||
|
||||
expect(part.exception).toBeNull();
|
||||
expect(part.isLoaded()).toBe(true);
|
||||
expect(part.isError()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -46,6 +46,7 @@ export abstract class FormPart<TPartState extends object, TFormState = any> impl
|
||||
this.schema = schema;
|
||||
|
||||
this.formState.submitTask.addHandler(executorHandlerFilter(() => this.isLoaded(), this.save.bind(this)));
|
||||
this.formState.prepareTask.addHandler(executorHandlerFilter(() => this.isLoaded(), this.prepare.bind(this)));
|
||||
this.formState.formatTask.addHandler(executorHandlerFilter(() => this.isLoaded() && this.isChanged, this.format.bind(this)));
|
||||
this.formState.validationTask.addHandler(executorHandlerFilter(() => this.isLoaded(), this.handleValidation.bind(this)));
|
||||
|
||||
@@ -203,6 +204,7 @@ export abstract class FormPart<TPartState extends object, TFormState = any> impl
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
protected prepare(data: IFormState<TFormState>, contexts: IExecutionContextProvider<IFormState<TFormState>>): void | Promise<void> {}
|
||||
protected format(data: IFormState<TFormState>, contexts: IExecutionContextProvider<IFormState<TFormState>>): void | Promise<void> {}
|
||||
protected validate(data: IFormState<TFormState>, contexts: IExecutionContextProvider<IFormState<TFormState>>): void | Promise<void> {}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ export class FormState<TState> implements IFormState<TState> {
|
||||
readonly formStateTask: IExecutor<TState>;
|
||||
readonly loadedTask: IExecutor<IFormState<TState>>;
|
||||
readonly submitTask: IExecutor<IFormState<TState>>;
|
||||
readonly prepareTask: IExecutor<IFormState<TState>>;
|
||||
readonly formatTask: IExecutor<IFormState<TState>>;
|
||||
readonly validationTask: IExecutor<IFormState<TState>>;
|
||||
readonly disposeTask: IExecutor<IFormState<TState>>;
|
||||
@@ -72,8 +73,11 @@ export class FormState<TState> implements IFormState<TState> {
|
||||
this.loadedTask = new Executor(this as IFormState<TState>, () => true);
|
||||
this.loadedTask.addCollection(service.onLoaded).next(this.formStateTask);
|
||||
|
||||
this.prepareTask = new Executor(this as IFormState<TState>, () => true);
|
||||
this.prepareTask.addCollection(service.onPrepare);
|
||||
|
||||
this.formatTask = new Executor(this as IFormState<TState>, () => true);
|
||||
this.formatTask.addCollection(service.onFormat);
|
||||
this.formatTask.addCollection(service.onFormat).before(this.prepareTask);
|
||||
|
||||
this.validationTask = new Executor(this as IFormState<TState>, () => true);
|
||||
this.validationTask.addCollection(service.onValidate).before(this.formatTask);
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface IFormState<TState> {
|
||||
readonly loadedTask: IExecutor<IFormState<TState>>;
|
||||
readonly formStateTask: IExecutor<TState>;
|
||||
readonly submitTask: IExecutor<IFormState<TState>>;
|
||||
readonly prepareTask: IExecutor<IFormState<TState>>;
|
||||
readonly formatTask: IExecutor<IFormState<TState>>;
|
||||
readonly validationTask: IExecutor<IFormState<TState>>;
|
||||
|
||||
|
||||
@@ -88,3 +88,4 @@ export * from './isNumber.js';
|
||||
export * from './getSubjectDifferences.js';
|
||||
export * from './downloadImage.js';
|
||||
export * from './submitForm.js';
|
||||
export * from './trimObjectValues.js';
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 { describe, expect, it } from 'vitest';
|
||||
|
||||
import { trimObjectValues } from './trimObjectValues.js';
|
||||
|
||||
describe('trimObjectValues', () => {
|
||||
it('trims string values in place', () => {
|
||||
const values = { text: ' value ', number: 1, empty: ' ' };
|
||||
|
||||
trimObjectValues(values);
|
||||
|
||||
expect(values).toEqual({ text: 'value', number: 1, empty: '' });
|
||||
});
|
||||
|
||||
it('does nothing when the object is undefined', () => {
|
||||
expect(() => trimObjectValues(undefined)).not.toThrow();
|
||||
expect(trimObjectValues(undefined)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
export function trimObjectValues(object: object | undefined): void {
|
||||
if (!object) {
|
||||
return;
|
||||
}
|
||||
|
||||
const values = object as Record<string, unknown>;
|
||||
|
||||
for (const key of Object.keys(values)) {
|
||||
const value = values[key];
|
||||
|
||||
if (typeof value === 'string') {
|
||||
values[key] = value.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { runInAction } from 'mobx';
|
||||
|
||||
import { FormMode, FormPart, type IFormState } from '@cloudbeaver/core-ui';
|
||||
import type { AiEngineConfig } from '@cloudbeaver/core-sdk';
|
||||
import { getUniqueName } from '@cloudbeaver/core-utils';
|
||||
import { getUniqueName, trimObjectValues } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { AIEnginePropertiesResource } from '../../AIEnginePropertiesResource.js';
|
||||
import { type AIAdminProfile, type AIProfileInput, AIProfilesResource } from '../../AIProfilesResource.js';
|
||||
@@ -90,13 +90,7 @@ export class AIProfileFormPart extends FormPart<IAIProfileOptionsState, IAIProfi
|
||||
this.state.name = getUniqueName(this.state.name, profileNames);
|
||||
}
|
||||
|
||||
for (const key of Object.keys(this.state.properties)) {
|
||||
const value = this.state.properties[key];
|
||||
|
||||
if (typeof value === 'string') {
|
||||
this.state.properties[key] = value.trim();
|
||||
}
|
||||
}
|
||||
trimObjectValues(this.state.properties);
|
||||
}
|
||||
|
||||
private getConfig(): AIProfileInput {
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { TeamInfo, TeamInfoMetaParametersResource, TeamsResource } from '@c
|
||||
import type { IExecutionContextProvider } from '@cloudbeaver/core-executor';
|
||||
import type { LocalizationService } from '@cloudbeaver/core-localization';
|
||||
import { FormMode, FormPart, formValidationContext, type IFormState } from '@cloudbeaver/core-ui';
|
||||
import { getUniqueName } from '@cloudbeaver/core-utils';
|
||||
import { getUniqueName, trimObjectValues } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { ITeamFormState } from '../TeamsAdministrationFormService.js';
|
||||
import type { ITeamOptionsState } from './ITeamOptionsState.js';
|
||||
@@ -92,13 +92,7 @@ export class TeamOptionsFormPart extends FormPart<ITeamOptionsState, ITeamFormSt
|
||||
this.state.description = this.state.description.trim();
|
||||
}
|
||||
|
||||
if (this.state.metaParameters) {
|
||||
for (const key of Object.keys(this.state.metaParameters)) {
|
||||
if (typeof this.state.metaParameters[key] === 'string') {
|
||||
this.state.metaParameters[key] = (this.state.metaParameters[key] as any).trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
trimObjectValues(this.state.metaParameters);
|
||||
}
|
||||
|
||||
protected override async saveChanges(
|
||||
|
||||
@@ -20,11 +20,10 @@ import type { TeamsAdministrationFormState } from './TeamsAdministrationFormStat
|
||||
interface Props {
|
||||
state: TeamsAdministrationFormState;
|
||||
onCancel?: () => void;
|
||||
onSave?: VoidFunction;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TeamForm = observer<Props>(function TeamForm({ state, onCancel, onSave = () => {}, className }) {
|
||||
export const TeamForm = observer<Props>(function TeamForm({ state, onCancel, className }) {
|
||||
const styles = useS(style);
|
||||
const service = useService(TeamsAdministrationFormService);
|
||||
const notificationService = useService(NotificationService);
|
||||
@@ -43,7 +42,6 @@ export const TeamForm = observer<Props>(function TeamForm({ state, onCancel, onS
|
||||
|
||||
notificationService.logSuccess({ title, message });
|
||||
|
||||
onSave?.();
|
||||
if (initialMode === FormMode.Create) {
|
||||
onCancel?.();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 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.
|
||||
@@ -19,7 +19,7 @@ export const CreateTeam: React.FC = observer(function CreateTeam() {
|
||||
const styles = useS(style);
|
||||
const service = useService(CreateTeamService);
|
||||
|
||||
if (!service.data) {
|
||||
if (!service.formState) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export const CreateTeam: React.FC = observer(function CreateTeam() {
|
||||
</GroupTitle>
|
||||
<Container overflow vertical>
|
||||
<Loader suspense>
|
||||
<TeamForm state={service.data} onCancel={service.cancelCreate} onSave={service.cancelCreate} />
|
||||
<TeamForm state={service.formState} onCancel={service.cancelCreate} />
|
||||
</Loader>
|
||||
</Container>
|
||||
</Group>
|
||||
|
||||
@@ -1,56 +1,89 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import { makeObservable, observable } from 'mobx';
|
||||
import { action, makeObservable, observable } from 'mobx';
|
||||
|
||||
import { ConfirmationDialog, importLazyComponent } from '@cloudbeaver/core-blocks';
|
||||
import { injectable, IServiceProvider } from '@cloudbeaver/core-di';
|
||||
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
|
||||
import { ExecutorInterrupter, type IExecutorHandler } from '@cloudbeaver/core-executor';
|
||||
import { OptionsPanelService, type OptionsPanelCloseEventData } from '@cloudbeaver/core-ui';
|
||||
|
||||
import { TeamsAdministrationNavService } from '../TeamsAdministrationNavService.js';
|
||||
import { TeamsAdministrationFormService } from '../TeamsForm/TeamsAdministrationFormService.js';
|
||||
import { TeamsAdministrationFormState } from '../TeamsForm/TeamsAdministrationFormState.js';
|
||||
|
||||
@injectable(() => [TeamsAdministrationNavService, IServiceProvider, TeamsAdministrationFormService])
|
||||
const CreateTeam = importLazyComponent(() => import('./CreateTeam.js').then(m => m.CreateTeam));
|
||||
const panelGetter = () => CreateTeam;
|
||||
|
||||
@injectable(() => [IServiceProvider, TeamsAdministrationFormService, OptionsPanelService, CommonDialogService])
|
||||
export class CreateTeamService {
|
||||
disabled = false;
|
||||
data: TeamsAdministrationFormState | null;
|
||||
formState: TeamsAdministrationFormState | null;
|
||||
|
||||
constructor(
|
||||
private readonly teamsAdministrationNavService: TeamsAdministrationNavService,
|
||||
private readonly serviceProvider: IServiceProvider,
|
||||
private readonly service: TeamsAdministrationFormService,
|
||||
private readonly optionsPanelService: OptionsPanelService,
|
||||
private readonly commonDialogService: CommonDialogService,
|
||||
) {
|
||||
this.data = null;
|
||||
this.formState = null;
|
||||
|
||||
this.cancelCreate = this.cancelCreate.bind(this);
|
||||
this.create = this.create.bind(this);
|
||||
this.optionsPanelService.closeTask.addHandler(this.closeHandler);
|
||||
|
||||
makeObservable(this, {
|
||||
data: observable,
|
||||
disabled: observable,
|
||||
formState: observable,
|
||||
cancelCreate: action.bound,
|
||||
create: action.bound,
|
||||
});
|
||||
}
|
||||
|
||||
cancelCreate(): void {
|
||||
this.teamsAdministrationNavService.navToRoot();
|
||||
async cancelCreate(): Promise<void> {
|
||||
await this.optionsPanelService.close();
|
||||
await this.dispose();
|
||||
}
|
||||
|
||||
fillData(): void {
|
||||
this.dispose();
|
||||
this.data = new TeamsAdministrationFormState(this.serviceProvider, this.service, {
|
||||
teamId: null,
|
||||
});
|
||||
async create(): Promise<void> {
|
||||
if (this.optionsPanelService.isOpen(panelGetter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const opened = await this.optionsPanelService.open(panelGetter);
|
||||
|
||||
if (opened) {
|
||||
await this.dispose();
|
||||
this.formState = new TeamsAdministrationFormState(this.serviceProvider, this.service, {
|
||||
teamId: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
create(): void {
|
||||
this.teamsAdministrationNavService.navToCreate();
|
||||
async dispose(): Promise<void> {
|
||||
await this.formState?.dispose();
|
||||
this.formState = null;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.data?.dispose();
|
||||
this.data = null;
|
||||
}
|
||||
private readonly closeHandler: IExecutorHandler<OptionsPanelCloseEventData> = async (data, contexts) => {
|
||||
if (data !== 'before' || !this.optionsPanelService.isOpen(panelGetter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.formState?.isChanged) {
|
||||
const { status } = await this.commonDialogService.open(ConfirmationDialog, {
|
||||
title: 'ui_discard_changes',
|
||||
message: 'ui_discard_changes_message',
|
||||
confirmActionText: 'ui_discard',
|
||||
cancelActionText: 'ui_keep_editing',
|
||||
});
|
||||
|
||||
if (status === DialogueStateResult.Rejected) {
|
||||
ExecutorInterrupter.interrupt(contexts);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await this.dispose();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -15,7 +15,6 @@ import { CachedMapAllKey } from '@cloudbeaver/core-resource';
|
||||
import { isDefined } from '@dbeaver/js-helpers';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
|
||||
import { CreateTeam } from './CreateTeam.js';
|
||||
import { CreateTeamService } from './CreateTeamService.js';
|
||||
import { useTeamsTable } from './useTeamsTable.js';
|
||||
import { TeamsTable } from './TeamsTable.js';
|
||||
@@ -24,7 +23,7 @@ interface Props {
|
||||
param?: string | null;
|
||||
}
|
||||
|
||||
export const TeamsPage = observer<Props>(function TeamsPage({ param }) {
|
||||
export const TeamsPage = observer<Props>(function TeamsPage() {
|
||||
const translate = useTranslate();
|
||||
const service = useService(CreateTeamService);
|
||||
|
||||
@@ -34,8 +33,6 @@ export const TeamsPage = observer<Props>(function TeamsPage({ param }) {
|
||||
const selection = useTableSelection(teams.map(t => t.teamId));
|
||||
const table = useTeamsTable(selection);
|
||||
|
||||
const create = param === 'create';
|
||||
|
||||
return (
|
||||
<ColoredContainer vertical wrap gap parent maximum>
|
||||
<Group box keepSize>
|
||||
@@ -44,7 +41,7 @@ export const TeamsPage = observer<Props>(function TeamsPage({ param }) {
|
||||
title={translate('administration_teams_add_tooltip')}
|
||||
icon="add"
|
||||
viewBox="0 0 24 24"
|
||||
disabled={create || table.processing}
|
||||
disabled={!!service.formState || table.processing}
|
||||
onClick={service.create}
|
||||
>
|
||||
{translate('ui_create')}
|
||||
@@ -71,11 +68,6 @@ export const TeamsPage = observer<Props>(function TeamsPage({ param }) {
|
||||
</Group>
|
||||
|
||||
<Container overflow gap maximum>
|
||||
{create && (
|
||||
<Group box>
|
||||
<CreateTeam />
|
||||
</Group>
|
||||
)}
|
||||
<TableSelectionContext value={selection}>
|
||||
<TeamsTable teams={teams} />
|
||||
</TableSelectionContext>
|
||||
|
||||
@@ -9,14 +9,11 @@ import React from 'react';
|
||||
|
||||
import { AdministrationItemService, type IAdministrationItem } from '@cloudbeaver/core-administration';
|
||||
import { type AdminUser, TeamsResource, UsersResource } from '@cloudbeaver/core-authentication';
|
||||
import { ConfirmationDialog, PlaceholderContainer } from '@cloudbeaver/core-blocks';
|
||||
import { PlaceholderContainer } from '@cloudbeaver/core-blocks';
|
||||
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
import { TabsContainer } from '@cloudbeaver/core-ui';
|
||||
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
|
||||
|
||||
import { CreateTeamService } from './Teams/TeamsTable/CreateTeamService.js';
|
||||
import { EUsersAdministrationSub, UsersAdministrationNavigationService } from './UsersAdministrationNavigationService.js';
|
||||
import { CreateUserService } from './UsersTable/CreateUserService.js';
|
||||
import type { IUserFilters } from './UsersTable/Filters/useUsersTableFilters.js';
|
||||
|
||||
const UserCredentialsList = React.lazy(async () => {
|
||||
@@ -47,7 +44,7 @@ export interface IUsersActionButtonProps {
|
||||
filters: IUserFilters;
|
||||
}
|
||||
|
||||
@injectable(() => [AdministrationItemService, CreateUserService, TeamsResource, CreateTeamService, UsersResource, CommonDialogService])
|
||||
@injectable(() => [AdministrationItemService, TeamsResource, UsersResource])
|
||||
export class UsersAdministrationService extends Bootstrap {
|
||||
readonly tabsContainer: TabsContainer;
|
||||
readonly userDetailsInfoPlaceholder: PlaceholderContainer<IUserDetailsInfoProps>;
|
||||
@@ -57,11 +54,8 @@ export class UsersAdministrationService extends Bootstrap {
|
||||
|
||||
constructor(
|
||||
private readonly administrationItemService: AdministrationItemService,
|
||||
private readonly createUserService: CreateUserService,
|
||||
private readonly teamsResource: TeamsResource,
|
||||
private readonly createTeamService: CreateTeamService,
|
||||
private readonly usersResource: UsersResource,
|
||||
private readonly commonDialogService: CommonDialogService,
|
||||
) {
|
||||
super();
|
||||
this.userDetailsInfoPlaceholder = new PlaceholderContainer();
|
||||
@@ -80,14 +74,11 @@ export class UsersAdministrationService extends Bootstrap {
|
||||
},
|
||||
{
|
||||
name: EUsersAdministrationSub.Users,
|
||||
canDeActivate: () => this.handleDeactivate(EUsersAdministrationSub.Users),
|
||||
onDeActivate: this.cancelUserCreate.bind(this),
|
||||
onDeActivate: this.handleUsersDeactivate.bind(this),
|
||||
},
|
||||
{
|
||||
name: EUsersAdministrationSub.Teams,
|
||||
canDeActivate: () => this.handleDeactivate(EUsersAdministrationSub.Teams),
|
||||
onActivate: this.loadTeams.bind(this),
|
||||
onDeActivate: this.cancelTeamCreate.bind(this),
|
||||
onDeActivate: this.handleTeamsDeactivate.bind(this),
|
||||
},
|
||||
],
|
||||
defaultSub: EUsersAdministrationSub.Users,
|
||||
@@ -98,53 +89,15 @@ export class UsersAdministrationService extends Bootstrap {
|
||||
this.actionButtonsPlaceholder.add(UsersTableFilterButton, 0);
|
||||
}
|
||||
|
||||
private async handleDeactivate(sub: EUsersAdministrationSub) {
|
||||
if (sub === EUsersAdministrationSub.Users || sub === EUsersAdministrationSub.Teams) {
|
||||
const users = sub === EUsersAdministrationSub.Users;
|
||||
const edited = users ? this.createUserService.state?.isChanged : this.createTeamService.data?.isChanged;
|
||||
|
||||
if (edited) {
|
||||
const { status } = await this.commonDialogService.open(ConfirmationDialog, {
|
||||
title: 'ui_discard_changes',
|
||||
message: 'ui_discard_changes_message',
|
||||
confirmActionText: 'ui_discard',
|
||||
cancelActionText: 'ui_keep_editing',
|
||||
});
|
||||
|
||||
if (status === DialogueStateResult.Rejected) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private cancelUserCreate(param: string | null, configurationWizard: boolean, outside: boolean) {
|
||||
if (param === 'create') {
|
||||
this.createUserService.close();
|
||||
}
|
||||
|
||||
private handleUsersDeactivate(param: string | null, configurationWizard: boolean, outside: boolean) {
|
||||
if (outside) {
|
||||
this.usersResource.cleanNewFlags();
|
||||
}
|
||||
}
|
||||
|
||||
private cancelTeamCreate(param: string | null, configurationWizard: boolean, outside: boolean) {
|
||||
if (param === 'create') {
|
||||
this.createTeamService.dispose();
|
||||
}
|
||||
|
||||
private handleTeamsDeactivate(param: string | null, configurationWizard: boolean, outside: boolean) {
|
||||
if (outside) {
|
||||
this.teamsResource.cleanNewFlags();
|
||||
}
|
||||
}
|
||||
|
||||
private loadTeams(param: string | null) {
|
||||
if (param === 'create') {
|
||||
this.createTeamService.fillData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,19 +8,19 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { Container, Group, GroupTitle, Loader, s, Translate, useS } from '@cloudbeaver/core-blocks';
|
||||
import type { IFormState } from '@cloudbeaver/core-ui';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
|
||||
import { AdministrationUserForm } from '../UserForm/AdministrationUserForm.js';
|
||||
import type { IUserFormState } from '../UserForm/AdministrationUserFormService.js';
|
||||
import style from './CreateUser.module.css';
|
||||
import { CreateUserService } from './CreateUserService.js';
|
||||
|
||||
interface Props {
|
||||
state: IFormState<IUserFormState>;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const CreateUser = observer<Props>(function CreateUser({ state, onCancel }) {
|
||||
export const CreateUser = observer(function CreateUser() {
|
||||
const styles = useS(style);
|
||||
const createUserService = useService(CreateUserService);
|
||||
|
||||
if (!createUserService.formState) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Group aria-labelledby="create-user-title" className={s(styles, { box: true })} gap vertical noWrap>
|
||||
@@ -29,7 +29,7 @@ export const CreateUser = observer<Props>(function CreateUser({ state, onCancel
|
||||
</GroupTitle>
|
||||
<Container overflow vertical>
|
||||
<Loader suspense>
|
||||
<AdministrationUserForm state={state} onClose={onCancel} />
|
||||
<AdministrationUserForm state={createUserService.formState} onClose={createUserService.cancelCreate} />
|
||||
</Loader>
|
||||
</Container>
|
||||
</Group>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import { AdministrationScreenService } from '@cloudbeaver/core-administration';
|
||||
import { AUTH_PROVIDER_LOCAL_ID, AuthProvidersResource } from '@cloudbeaver/core-authentication';
|
||||
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
import { CachedMapAllKey, getCachedMapResourceLoaderState } from '@cloudbeaver/core-resource';
|
||||
@@ -13,7 +12,6 @@ import { ACTION_CREATE, ACTION_REFRESH, ActionService, menuExtractItems, MenuSer
|
||||
|
||||
import { AdministrationUsersManagementService } from '../../../AdministrationUsersManagementService.js';
|
||||
import { MENU_USERS_ADMINISTRATION } from '../../../Menus/MENU_USERS_ADMINISTRATION.js';
|
||||
import { ADMINISTRATION_ITEM_USER_CREATE_PARAM } from '../ADMINISTRATION_ITEM_USER_CREATE_PARAM.js';
|
||||
import { CreateUserService } from './CreateUserService.js';
|
||||
import { DATA_CONTEXT_USERS_ADMINISTRATION_ACTIONS } from './DATA_CONTEXT_USERS_ADMINISTRATION_ACTIONS.js';
|
||||
|
||||
@@ -23,7 +21,6 @@ import { DATA_CONTEXT_USERS_ADMINISTRATION_ACTIONS } from './DATA_CONTEXT_USERS_
|
||||
MenuService,
|
||||
ActionService,
|
||||
AdministrationUsersManagementService,
|
||||
AdministrationScreenService,
|
||||
])
|
||||
export class CreateUserBootstrap extends Bootstrap {
|
||||
constructor(
|
||||
@@ -32,7 +29,6 @@ export class CreateUserBootstrap extends Bootstrap {
|
||||
private readonly menuService: MenuService,
|
||||
private readonly actionService: ActionService,
|
||||
private readonly administrationUsersManagementService: AdministrationUsersManagementService,
|
||||
private readonly administrationScreenService: AdministrationScreenService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -62,7 +58,7 @@ export class CreateUserBootstrap extends Bootstrap {
|
||||
},
|
||||
isDisabled: (context, action) => {
|
||||
if (action === ACTION_CREATE) {
|
||||
return this.administrationScreenService.activeScreen?.param === ADMINISTRATION_ITEM_USER_CREATE_PARAM && !!this.createUserService.state;
|
||||
return !!this.createUserService.formState;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -1,65 +1,93 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import { makeObservable, observable } from 'mobx';
|
||||
import { action, makeObservable, observable } from 'mobx';
|
||||
|
||||
import { PlaceholderContainer } from '@cloudbeaver/core-blocks';
|
||||
import { ConfirmationDialog, importLazyComponent, PlaceholderContainer } from '@cloudbeaver/core-blocks';
|
||||
import { injectable, IServiceProvider } from '@cloudbeaver/core-di';
|
||||
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
|
||||
import { ExecutorInterrupter, type IExecutorHandler } from '@cloudbeaver/core-executor';
|
||||
import { OptionsPanelService, type OptionsPanelCloseEventData } from '@cloudbeaver/core-ui';
|
||||
|
||||
import { AdministrationUserFormService } from '../UserForm/AdministrationUserFormService.js';
|
||||
import { AdministrationUserFormState } from '../UserForm/AdministrationUserFormState.js';
|
||||
import { UsersAdministrationNavigationService } from '../UsersAdministrationNavigationService.js';
|
||||
|
||||
const CreateUser = importLazyComponent(() => import('./CreateUser.js').then(m => m.CreateUser));
|
||||
const panelGetter = () => CreateUser;
|
||||
|
||||
export interface IToolsContainerProps {
|
||||
param: string | null | undefined;
|
||||
}
|
||||
|
||||
@injectable(() => [IServiceProvider, AdministrationUserFormService, UsersAdministrationNavigationService])
|
||||
@injectable(() => [IServiceProvider, AdministrationUserFormService, OptionsPanelService, CommonDialogService])
|
||||
export class CreateUserService {
|
||||
state: AdministrationUserFormState | null;
|
||||
formState: AdministrationUserFormState | null;
|
||||
readonly toolsContainer: PlaceholderContainer<IToolsContainerProps>;
|
||||
|
||||
constructor(
|
||||
private readonly serviceProvider: IServiceProvider,
|
||||
private readonly administrationUserFormService: AdministrationUserFormService,
|
||||
private readonly usersAdministrationNavigationService: UsersAdministrationNavigationService,
|
||||
private readonly optionsPanelService: OptionsPanelService,
|
||||
private readonly commonDialogService: CommonDialogService,
|
||||
) {
|
||||
this.toolsContainer = new PlaceholderContainer();
|
||||
this.state = null;
|
||||
this.formState = null;
|
||||
|
||||
this.optionsPanelService.closeTask.addHandler(this.closeHandler);
|
||||
|
||||
makeObservable(this, {
|
||||
state: observable,
|
||||
formState: observable,
|
||||
cancelCreate: action.bound,
|
||||
create: action.bound,
|
||||
});
|
||||
|
||||
this.clearUserTemplate = this.clearUserTemplate.bind(this);
|
||||
this.cancelCreate = this.cancelCreate.bind(this);
|
||||
this.create = this.create.bind(this);
|
||||
}
|
||||
|
||||
cancelCreate(): void {
|
||||
this.clearUserTemplate();
|
||||
this.usersAdministrationNavigationService.navToRoot();
|
||||
async cancelCreate(): Promise<void> {
|
||||
await this.optionsPanelService.close();
|
||||
await this.dispose();
|
||||
}
|
||||
|
||||
create(): void {
|
||||
if (this.state) {
|
||||
async create(): Promise<void> {
|
||||
if (this.optionsPanelService.isOpen(panelGetter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.state = new AdministrationUserFormState(this.serviceProvider, this.administrationUserFormService, { userId: null });
|
||||
this.usersAdministrationNavigationService.navToCreate();
|
||||
const opened = await this.optionsPanelService.open(panelGetter);
|
||||
|
||||
if (opened) {
|
||||
await this.dispose();
|
||||
this.formState = new AdministrationUserFormState(this.serviceProvider, this.administrationUserFormService, { userId: null });
|
||||
}
|
||||
}
|
||||
|
||||
clearUserTemplate(): void {
|
||||
this.state?.dispose();
|
||||
this.state = null;
|
||||
async dispose(): Promise<void> {
|
||||
await this.formState?.dispose();
|
||||
this.formState = null;
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.clearUserTemplate();
|
||||
}
|
||||
private readonly closeHandler: IExecutorHandler<OptionsPanelCloseEventData> = async (data, contexts) => {
|
||||
if (data !== 'before' || !this.optionsPanelService.isOpen(panelGetter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.formState?.isChanged) {
|
||||
const { status } = await this.commonDialogService.open(ConfirmationDialog, {
|
||||
title: 'ui_discard_changes',
|
||||
message: 'ui_discard_changes_message',
|
||||
confirmActionText: 'ui_discard',
|
||||
cancelActionText: 'ui_keep_editing',
|
||||
});
|
||||
|
||||
if (status === DialogueStateResult.Rejected) {
|
||||
ExecutorInterrupter.interrupt(contexts);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await this.dispose();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -14,7 +14,6 @@ import { useService } from '@cloudbeaver/core-di';
|
||||
import { Spinner } from '@dbeaver/ui-kit';
|
||||
|
||||
import { AdministrationUsersManagementService } from '../../../AdministrationUsersManagementService.js';
|
||||
import { CreateUser } from './CreateUser.js';
|
||||
import { CreateUserService } from './CreateUserService.js';
|
||||
import { UsersTableFilters } from './Filters/UsersTableFilters.js';
|
||||
import { useUsersTableFilters } from './Filters/useUsersTableFilters.js';
|
||||
@@ -37,7 +36,6 @@ export const UsersPage = observer<Props>(function UsersPage({ param }) {
|
||||
const filters = useUsersTableFilters();
|
||||
const table = useUsersTable(filters);
|
||||
|
||||
const create = param === 'create';
|
||||
const displayAuthRole = authRolesResource.data.length > 0;
|
||||
const loading = table.loadableState.isLoading();
|
||||
const isManageable = !administrationUsersManagementService.externalUserProviderEnabled;
|
||||
@@ -59,10 +57,6 @@ export const UsersPage = observer<Props>(function UsersPage({ param }) {
|
||||
</Group>
|
||||
|
||||
<Container overflow gap maximum>
|
||||
{create && createUserService.state && isManageable && (
|
||||
<CreateUser state={createUserService.state} onCancel={createUserService.cancelCreate} />
|
||||
)}
|
||||
|
||||
<Placeholder container={createUserService.toolsContainer} param={param} />
|
||||
|
||||
<div className="tw:h-full tw:relative">
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M10.5 5.5C7.3 6.2 6.2 7.3 5.5 10.5 4.8 7.3 3.7 6.2.5 5.5 3.7 4.8 4.8 3.7 5.5.5c.7 3.2 1.8 4.3 5 5ZM21.5 15.5c-3.84.84-5.16 2.16-6 6-.84-3.84-2.16-5.16-6-6 3.84-.84 5.16-2.16 6-6 .84 3.84 2.16 5.16 6 6ZM22 5c-1.92.42-2.58 1.08-3 3-.42-1.92-1.08-2.58-3-3 1.92-.42 2.58-1.08 3-3 .42 1.92 1.08 2.58 3 3Z" stroke="#6B7280" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 500 B |
@@ -84,6 +84,8 @@ export class PluginBootstrap extends Bootstrap {
|
||||
this.connectionFormService.parts.add({
|
||||
key: this.key,
|
||||
name: 'plugin_connection_form_ai_name',
|
||||
icon: '/icons/plugin_connection_sparkles.svg',
|
||||
order: 7,
|
||||
stateGetter: context => () => getConnectionAiPart(context.formState),
|
||||
getLoader: () => [getCachedDataResourceLoaderState(this.serverConfigResource, () => undefined)],
|
||||
isHidden: (_, context) => !context || !this.isAiEnabled(),
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { Checkbox, ColoredContainer, Container, Group, Placeholder, useAutoLoad, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import { getConnectionFormOptionsPart, type IConnectionFormProps } from '@cloudbeaver/plugin-connections';
|
||||
import { Checkbox, Placeholder, useAutoLoad, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import { ConnectionSectionWrapper, getConnectionFormOptionsPart, type IConnectionFormProps } from '@cloudbeaver/plugin-connections';
|
||||
import { getConnectionAiPart } from './getConnectionAiPart.js';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
@@ -25,20 +25,16 @@ export const ConnectionAiForm: TabContainerPanelComponent<IConnectionFormProps>
|
||||
useAutoLoad(ConnectionAiForm, [optionsFormPart, aiPart], selected);
|
||||
|
||||
return (
|
||||
<ColoredContainer wrap overflow parent gap>
|
||||
<Container medium gap>
|
||||
<Group>
|
||||
<Container>
|
||||
<Checkbox
|
||||
title={translate('plugin_connection_form_ai_form_transfer_description')}
|
||||
state={aiPart.state}
|
||||
name="metaTransferConfirmed"
|
||||
label={translate('plugin_connection_form_ai_form_transfer_enabled')}
|
||||
/>
|
||||
</Container>
|
||||
{selected && <Placeholder container={connectionFormAiService.optionsContainer} formState={formState} />}
|
||||
</Group>
|
||||
</Container>
|
||||
</ColoredContainer>
|
||||
<div className="tw:flex tw:flex-1 tw:overflow-auto">
|
||||
<ConnectionSectionWrapper>
|
||||
<Checkbox
|
||||
title={translate('plugin_connection_form_ai_form_transfer_description')}
|
||||
state={aiPart.state}
|
||||
name="metaTransferConfirmed"
|
||||
label={translate('plugin_connection_form_ai_form_transfer_enabled')}
|
||||
/>
|
||||
{selected && <Placeholder container={connectionFormAiService.optionsContainer} formState={formState} />}
|
||||
</ConnectionSectionWrapper>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<rect x="4.5" y="10" width="15" height="11" rx="2" stroke="#6B7280" stroke-width="1.8"/>
|
||||
<path d="M8 10V7.5a4 4 0 0 1 8 0V10M12 14v3" stroke="#6B7280" stroke-width="1.8" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 302 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 2.75c2.2 1.7 4.85 2.64 7.63 2.7v5.29c0 4.63-3.08 8.46-7.63 10.51-4.55-2.05-7.63-5.88-7.63-10.51V5.45A12.4 12.4 0 0 0 12 2.75Z" stroke="#6B7280" stroke-width="1.8" stroke-linejoin="round"/>
|
||||
<path d="m9 10 2 2-2 2M13 14h2" stroke="#6B7280" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 426 B |
@@ -16,6 +16,7 @@ import {
|
||||
NetworkHandlerResource,
|
||||
SSH_DEFAULT_HANDLER_CONFIG,
|
||||
SSH_TUNNEL_ID,
|
||||
trimSSHConfig,
|
||||
validateSSHConfig,
|
||||
type INetworkHandlerConfig,
|
||||
} from '@cloudbeaver/plugin-network-handlers';
|
||||
@@ -86,7 +87,7 @@ export class ConnectionFormSSHPart extends FormPart<INetworkHandlerConfig, IConn
|
||||
contexts: IExecutionContextProvider<IFormState<IConnectionFormState>>,
|
||||
): Promise<void> {}
|
||||
|
||||
protected override format(
|
||||
protected override prepare(
|
||||
data: IFormState<IConnectionFormState>,
|
||||
contexts: IExecutionContextProvider<IFormState<IConnectionFormState>>,
|
||||
): void | Promise<void> {
|
||||
@@ -107,6 +108,14 @@ export class ConnectionFormSSHPart extends FormPart<INetworkHandlerConfig, IConn
|
||||
this.optionsPart.state.networkHandlersConfig!.push(config);
|
||||
}
|
||||
|
||||
protected override format(): void {
|
||||
const index = this.optionsPart.state.networkHandlersConfig?.findIndex(config => config.id === this.state.id) ?? -1;
|
||||
if (index >= 0) {
|
||||
const config = this.optionsPart.state.networkHandlersConfig![index]!;
|
||||
this.optionsPart.state.networkHandlersConfig![index] = trimSSHConfig(config);
|
||||
}
|
||||
}
|
||||
|
||||
protected override validate(
|
||||
data: IFormState<IConnectionFormState>,
|
||||
contexts: IExecutionContextProvider<IFormState<IConnectionFormState>>,
|
||||
|
||||
@@ -31,7 +31,7 @@ export class ConnectionSSHTabService extends Bootstrap {
|
||||
this.connectionFormService.parts.add({
|
||||
key: 'ssh',
|
||||
name: 'plugin_connections_connection_form_part_main',
|
||||
order: 2.5,
|
||||
order: 3,
|
||||
tab: () => SSHTab,
|
||||
panel: () => SSHPanel,
|
||||
getLoader: (_, props) => {
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
*/
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { ColoredContainer, Form, Group, s, useAutoLoad, useS } from '@cloudbeaver/core-blocks';
|
||||
import { Form, s, useAutoLoad, useS } from '@cloudbeaver/core-blocks';
|
||||
import { type NetworkHandlerConfigInput } from '@cloudbeaver/core-sdk';
|
||||
import { SSHForm } from '@cloudbeaver/plugin-network-handlers';
|
||||
import { useTab, type IFormState, type TabContainerPanelComponent } from '@cloudbeaver/core-ui';
|
||||
|
||||
import styles from './SSH.module.css';
|
||||
import { getConnectionFormSSHPart } from './getConnectionFormSSHPart.js';
|
||||
import { getConnectionFormOptionsPart, type IConnectionFormState } from '@cloudbeaver/plugin-connections';
|
||||
import { ConnectionSectionWrapper, getConnectionFormOptionsPart, type IConnectionFormState } from '@cloudbeaver/plugin-connections';
|
||||
|
||||
interface Props {
|
||||
handlerState: NetworkHandlerConfigInput;
|
||||
@@ -31,19 +31,17 @@ export const SSH: TabContainerPanelComponent<Props> = observer(function SSH({ fo
|
||||
|
||||
return (
|
||||
<Form className={s(style, { form: true })}>
|
||||
<ColoredContainer parent>
|
||||
<Group form gap keepSize large>
|
||||
<SSHForm
|
||||
state={handlerState}
|
||||
initialState={sshPart.initialState}
|
||||
disabled={formState.isDisabled || formState.isReadOnly}
|
||||
readonly={formState.isReadOnly || sshPart.isReadOnly}
|
||||
sharedCredentials={optionsPart.state.sharedCredentials}
|
||||
projectId={formState.state.projectId}
|
||||
connectionId={formState.state.connectionId}
|
||||
/>
|
||||
</Group>
|
||||
</ColoredContainer>
|
||||
<ConnectionSectionWrapper className="tw:block! tw:space-y-4">
|
||||
<SSHForm
|
||||
state={handlerState}
|
||||
initialState={sshPart.initialState}
|
||||
disabled={formState.isDisabled || formState.isReadOnly}
|
||||
readonly={formState.isReadOnly || sshPart.isReadOnly}
|
||||
sharedCredentials={optionsPart.state.sharedCredentials}
|
||||
projectId={formState.state.projectId}
|
||||
connectionId={formState.state.connectionId}
|
||||
/>
|
||||
</ConnectionSectionWrapper>
|
||||
</Form>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { Translate, useResource } from '@cloudbeaver/core-blocks';
|
||||
import { NetworkHandlerResource, SSH_TUNNEL_ID } from '@cloudbeaver/plugin-network-handlers';
|
||||
import { Tab, type TabContainerTabComponent, TabTitle, useTab } from '@cloudbeaver/core-ui';
|
||||
import { Tab, type TabContainerTabComponent, TabIcon, TabTitle, useTab } from '@cloudbeaver/core-ui';
|
||||
import type { IConnectionFormProps } from '@cloudbeaver/plugin-connections';
|
||||
|
||||
export const SSHTab: TabContainerTabComponent<IConnectionFormProps> = observer(function SSHTab(props) {
|
||||
@@ -20,6 +20,7 @@ export const SSHTab: TabContainerTabComponent<IConnectionFormProps> = observer(f
|
||||
|
||||
return (
|
||||
<Tab {...props} title={handler.data?.description}>
|
||||
<TabIcon icon="/icons/plugin_connection_shield_terminal.svg" viewBox="0 0 16 16" />
|
||||
<TabTitle>
|
||||
<Translate token={handler.data?.label || 'plugin_connection_network_handlers_ssh_tunnel_title'} />
|
||||
</TabTitle>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { getSSLDriverHandler } from './getSSLDriverHandler.js';
|
||||
import { ConnectionInfoNetworkHandlersResource, type DBDriverResource } from '@cloudbeaver/core-connections';
|
||||
import { type INetworkHandlerConfig, type NetworkHandlerResource } from '@cloudbeaver/plugin-network-handlers';
|
||||
import { CachedMapAllKey } from '@cloudbeaver/core-resource';
|
||||
import { trimObjectValues } from '@cloudbeaver/core-utils';
|
||||
import { makeObservable, observable, toJS } from 'mobx';
|
||||
import { ConnectionFormOptionsPart, PROPERTY_FEATURE_SECURED, type IConnectionFormState } from '@cloudbeaver/plugin-connections';
|
||||
import { SSL_CODE_NAME } from './SSL_CODE_NAME.js';
|
||||
@@ -95,7 +96,7 @@ export class ConnectionFormSSLPart extends FormPart<INetworkHandlerConfig, IConn
|
||||
this.setInitialState(initialConfig ?? getDefaultState());
|
||||
}
|
||||
|
||||
protected override async format(
|
||||
protected override async prepare(
|
||||
data: IFormState<IConnectionFormState>,
|
||||
contexts: IExecutionContextProvider<IFormState<IConnectionFormState>>,
|
||||
): Promise<void> {
|
||||
@@ -158,10 +159,15 @@ export class ConnectionFormSSLPart extends FormPart<INetworkHandlerConfig, IConn
|
||||
this.formState.state.requiredNetworkHandlersIds = this.formState.state.requiredNetworkHandlersIds.filter(id => id !== this.state.id);
|
||||
}
|
||||
|
||||
this.optionsPart.state.networkHandlersConfig!.push(handlerConfig);
|
||||
}
|
||||
|
||||
protected override format(): void {
|
||||
trimSSLConfig(this.state);
|
||||
|
||||
const handlerConfig = this.optionsPart.state.networkHandlersConfig?.find(config => config.id === this.state.id);
|
||||
if (handlerConfig) {
|
||||
trimSSLConfig(handlerConfig);
|
||||
|
||||
this.optionsPart.state.networkHandlersConfig!.push(handlerConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,15 +184,7 @@ function trimSSLConfig(input: INetworkHandlerConfig): INetworkHandlerConfig {
|
||||
return input;
|
||||
}
|
||||
|
||||
if (!Object.keys(secureProperties).length) {
|
||||
return input;
|
||||
}
|
||||
|
||||
for (const key in secureProperties) {
|
||||
if (typeof secureProperties[key] === 'string') {
|
||||
secureProperties[key] = secureProperties[key]?.trim();
|
||||
}
|
||||
}
|
||||
trimObjectValues(secureProperties);
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
@@ -10,10 +10,8 @@ import { observer } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
ColoredContainer,
|
||||
FieldCheckbox,
|
||||
Form,
|
||||
Group,
|
||||
GroupTitle,
|
||||
IconOrImage,
|
||||
ObjectPropertyInfoForm,
|
||||
@@ -36,7 +34,7 @@ import { WEBSITE_LINKS } from '@cloudbeaver/core-links';
|
||||
|
||||
import styles from './SSL.module.css';
|
||||
import { ConnectionInfoNetworkHandlersResource } from '@cloudbeaver/core-connections';
|
||||
import { getConnectionFormOptionsPart, type IConnectionFormProps } from '@cloudbeaver/plugin-connections';
|
||||
import { ConnectionSectionWrapper, getConnectionFormOptionsPart, type IConnectionFormProps } from '@cloudbeaver/plugin-connections';
|
||||
import { SSLDescription } from './SSLDescription.js';
|
||||
|
||||
interface Props extends IConnectionFormProps {
|
||||
@@ -71,70 +69,66 @@ export const SSL: TabContainerPanelComponent<Props> = observer(function SSL({ fo
|
||||
|
||||
return (
|
||||
<Form className={s(style, { form: true })}>
|
||||
<ColoredContainer parent>
|
||||
<Group gap form large vertical>
|
||||
<Switch id="ssl-enable-switch" name="enabled" state={handlerState} description={<SSLDescription />} mod={['primary']} disabled={disabled}>
|
||||
{translate('plugin_connection_network_handlers_connection_ssl_enable')}
|
||||
</Switch>
|
||||
{isUncategorizedExists && (
|
||||
<ConnectionSectionWrapper>
|
||||
<Switch id="ssl-enable-switch" name="enabled" state={handlerState} description={<SSLDescription />} mod={['primary']} disabled={disabled}>
|
||||
{translate('plugin_connection_network_handlers_connection_ssl_enable')}
|
||||
</Switch>
|
||||
{isUncategorizedExists && (
|
||||
<ObjectPropertyInfoForm
|
||||
state={handlerState.properties}
|
||||
properties={handler.properties}
|
||||
category={null}
|
||||
disabled={disabled || !enabled}
|
||||
isSaved={p => !!p.id && initialHandler?.secureProperties[p.id] === SAVED_VALUE_INDICATOR}
|
||||
autocompleteSectionName="section-ssl"
|
||||
hideEmptyPlaceholder
|
||||
showRememberTip
|
||||
/>
|
||||
)}
|
||||
{categories.map(category => (
|
||||
<React.Fragment key={category}>
|
||||
<GroupTitle keepSize>{category}</GroupTitle>
|
||||
<ObjectPropertyInfoForm
|
||||
state={handlerState.properties}
|
||||
properties={handler.properties}
|
||||
category={null}
|
||||
category={category}
|
||||
disabled={disabled || !enabled}
|
||||
isSaved={p => !!p.id && initialHandler?.secureProperties[p.id] === SAVED_VALUE_INDICATOR}
|
||||
autocompleteSectionName="section-ssl"
|
||||
hideEmptyPlaceholder
|
||||
showRememberTip
|
||||
small
|
||||
/>
|
||||
)}
|
||||
{categories.map(category => (
|
||||
<React.Fragment key={category}>
|
||||
<GroupTitle keepSize>{category}</GroupTitle>
|
||||
<ObjectPropertyInfoForm
|
||||
state={handlerState.properties}
|
||||
properties={handler.properties}
|
||||
category={category}
|
||||
disabled={disabled || !enabled}
|
||||
isSaved={p => !!p.id && initialHandler?.secureProperties[p.id] === SAVED_VALUE_INDICATOR}
|
||||
autocompleteSectionName="section-ssl"
|
||||
hideEmptyPlaceholder
|
||||
showRememberTip
|
||||
small
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
{canSave && !optionsPart.state.sharedCredentials && (
|
||||
<FieldCheckbox
|
||||
id={handler.id + '_savePassword'}
|
||||
name="savePassword"
|
||||
state={handlerState}
|
||||
disabled={disabled || !enabled || optionsPart.state.sharedCredentials}
|
||||
title={translate(
|
||||
!isSharedProject || serverConfigResource.data?.distributed
|
||||
? 'plugin_connection_network_handlers_save_credentials_for_user_tooltip'
|
||||
: 'plugin_connection_network_handlers_save_credentials_shared_tooltip',
|
||||
)}
|
||||
>
|
||||
{translate(
|
||||
!isSharedProject || serverConfigResource.data?.distributed
|
||||
? 'plugin_connection_network_handlers_save_credentials_for_user'
|
||||
: 'plugin_connection_network_handlers_save_credentials_shared',
|
||||
)}
|
||||
</FieldCheckbox>
|
||||
)}
|
||||
|
||||
<a
|
||||
className="tw:flex tw:items-center tw:gap-2 tw:text-balance"
|
||||
href={WEBSITE_LINKS.SSL_CONFIGURATION_DOCUMENTATION_PAGE}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
</React.Fragment>
|
||||
))}
|
||||
{canSave && !optionsPart.state.sharedCredentials && (
|
||||
<FieldCheckbox
|
||||
id={handler.id + '_savePassword'}
|
||||
name="savePassword"
|
||||
state={handlerState}
|
||||
disabled={disabled || !enabled || optionsPart.state.sharedCredentials}
|
||||
title={translate(
|
||||
!isSharedProject || serverConfigResource.data?.distributed
|
||||
? 'plugin_connection_network_handlers_save_credentials_for_user_tooltip'
|
||||
: 'plugin_connection_network_handlers_save_credentials_shared_tooltip',
|
||||
)}
|
||||
>
|
||||
<IconOrImage width={16} icon="/icons/documentation_link_sm.svg" /> {translate('plugin_connection_network_handlers_connection_ssl_docs')}
|
||||
</a>
|
||||
</Group>
|
||||
</ColoredContainer>
|
||||
{translate(
|
||||
!isSharedProject || serverConfigResource.data?.distributed
|
||||
? 'plugin_connection_network_handlers_save_credentials_for_user'
|
||||
: 'plugin_connection_network_handlers_save_credentials_shared',
|
||||
)}
|
||||
</FieldCheckbox>
|
||||
)}
|
||||
|
||||
<a
|
||||
className="tw:flex tw:items-center tw:gap-2 tw:text-balance"
|
||||
href={WEBSITE_LINKS.SSL_CONFIGURATION_DOCUMENTATION_PAGE}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<IconOrImage width={16} icon="/icons/documentation_link_sm.svg" /> {translate('plugin_connection_network_handlers_connection_ssl_docs')}
|
||||
</a>
|
||||
</ConnectionSectionWrapper>
|
||||
</Form>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Translate, useResource } from '@cloudbeaver/core-blocks';
|
||||
import { DBDriverResource } from '@cloudbeaver/core-connections';
|
||||
import { NetworkHandlerResource } from '@cloudbeaver/plugin-network-handlers';
|
||||
import { CachedMapAllKey } from '@cloudbeaver/core-resource';
|
||||
import { Tab, type TabContainerTabComponent, TabTitle, useTab } from '@cloudbeaver/core-ui';
|
||||
import { Tab, type TabContainerTabComponent, TabIcon, TabTitle, useTab } from '@cloudbeaver/core-ui';
|
||||
|
||||
import { getSSLDriverHandler } from './getSSLDriverHandler.js';
|
||||
import { getConnectionFormOptionsPart, type IConnectionFormProps } from '@cloudbeaver/plugin-connections';
|
||||
@@ -33,6 +33,7 @@ export const SSLTab: TabContainerTabComponent<IConnectionFormProps> = observer(f
|
||||
|
||||
return (
|
||||
<Tab {...props} title={handler.description}>
|
||||
<TabIcon icon="/icons/plugin_connection_padlock.svg" viewBox="0 0 16 16" />
|
||||
<TabTitle>
|
||||
<Translate token={handler.label} />
|
||||
</TabTitle>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { Container, FieldCheckbox, Group, GroupTitle, Radio, RadioGroup, useAutoLoad, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import { FieldCheckbox, GroupTitle, Radio, RadioGroup, useAutoLoad, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import { CONNECTION_NAVIGATOR_VIEW_SETTINGS, isNavigatorViewSettingsEqual } from '@cloudbeaver/core-root';
|
||||
import { getConnectionFormOptionsPart, type ConnectionFormContainerProps } from '@cloudbeaver/plugin-connections';
|
||||
|
||||
@@ -30,14 +30,19 @@ export const ConnectionViewForm = observer<ConnectionFormContainerProps>(functio
|
||||
}
|
||||
|
||||
return (
|
||||
<Group form gap>
|
||||
<div className="tw:flex tw:flex-col tw:gap-4">
|
||||
<GroupTitle>{translate('plugin_connection_view_default')}</GroupTitle>
|
||||
<Container gap dense>
|
||||
<RadioGroup name='conection_view' aria-label={translate('plugin_connection_view_default')} value={isSimple ? 'simple' : 'advanced'} onChange={changeView}>
|
||||
<Radio value='simple' disabled={disabled} title={translate('plugin_connection_view_option_simple_description')} small keepSize>
|
||||
<div className="tw:flex tw:flex-col tw:gap-2">
|
||||
<RadioGroup
|
||||
name="conection_view"
|
||||
aria-label={translate('plugin_connection_view_default')}
|
||||
value={isSimple ? 'simple' : 'advanced'}
|
||||
onChange={changeView}
|
||||
>
|
||||
<Radio value="simple" disabled={disabled} title={translate('plugin_connection_view_option_simple_description')} small keepSize>
|
||||
{translate('plugin_connection_view_option_simple')}
|
||||
</Radio>
|
||||
<Radio value='advanced' disabled={disabled} title={translate('plugin_connection_view_option_advanced_description')} small keepSize>
|
||||
<Radio value="advanced" disabled={disabled} title={translate('plugin_connection_view_option_advanced_description')} small keepSize>
|
||||
{translate('plugin_connection_view_option_advanced')}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
@@ -45,7 +50,7 @@ export const ConnectionViewForm = observer<ConnectionFormContainerProps>(functio
|
||||
<FieldCheckbox disabled={disabled} name="showSystemObjects" state={viewFormPart.state}>
|
||||
{translate('plugin_connection_view_option_show_system_objects')}
|
||||
</FieldCheckbox>
|
||||
</Container>
|
||||
</Group>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 2.75c2.2 1.7 4.85 2.64 7.63 2.7v5.29c0 4.63-3.08 8.46-7.63 10.51-4.55-2.05-7.63-5.88-7.63-10.51V5.45A12.4 12.4 0 0 0 12 2.75Z" stroke="#6B7280" stroke-width="1.8" stroke-linejoin="round"/>
|
||||
<circle cx="12" cy="10" r="2.25" stroke="#6B7280" stroke-width="1.8"/>
|
||||
<path d="M8.75 16c.62-1.55 1.7-2.32 3.25-2.32s2.63.77 3.25 2.32" stroke="#6B7280" stroke-width="1.8" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 508 B |
@@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { Alert, StaticImage, useAutoLoad, useResource, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import { useTab, type TabContainerPanelComponent } from '@cloudbeaver/core-ui';
|
||||
import { getConnectionFormOptionsPart, type IConnectionFormProps } from '@cloudbeaver/plugin-connections';
|
||||
import { ConnectionSectionWrapper, getConnectionFormOptionsPart, type IConnectionFormProps } from '@cloudbeaver/plugin-connections';
|
||||
import { CachedMapAllKey, CachedResourceOffsetPageListKey } from '@cloudbeaver/core-resource';
|
||||
import {
|
||||
compareGrantSubjectsByLastLogin,
|
||||
@@ -73,7 +73,13 @@ export const ConnectionAccessTable: TabContainerPanelComponent<IConnectionFormPr
|
||||
const cloud = connectionInfo && originInfo?.origin ? isCloudConnection(originInfo.origin) : false;
|
||||
|
||||
if (cloud) {
|
||||
return <Alert className="tw:h-max tw:m-4">{translate('cloud_connections_access_placeholder')}</Alert>;
|
||||
return (
|
||||
<div className="tw:flex tw:flex-1 tw:overflow-auto">
|
||||
<ConnectionSectionWrapper>
|
||||
<Alert className="tw:h-max">{translate('cloud_connections_access_placeholder')}</Alert>
|
||||
</ConnectionSectionWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const users = userLoader.data as AdminUser[];
|
||||
@@ -140,19 +146,21 @@ export const ConnectionAccessTable: TabContainerPanelComponent<IConnectionFormPr
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tw:flex tw:flex-col tw:gap-4 tw:overflow-auto tw:p-4">
|
||||
<GrantManagementTable
|
||||
items={items}
|
||||
columns={COLUMNS}
|
||||
getItemId={item => ('teamId' in item ? item.teamId : item.userId)}
|
||||
isGranted={isGranted}
|
||||
isEdited={isEdited}
|
||||
getCell={getCell}
|
||||
isVisible={isVisible}
|
||||
disabled={formState.isDisabled}
|
||||
onGrant={accessPart.grant}
|
||||
onRevoke={accessPart.revoke}
|
||||
/>
|
||||
<div className="tw:flex tw:flex-1 tw:overflow-auto">
|
||||
<ConnectionSectionWrapper>
|
||||
<GrantManagementTable
|
||||
items={items}
|
||||
columns={COLUMNS}
|
||||
getItemId={item => ('teamId' in item ? item.teamId : item.userId)}
|
||||
isGranted={isGranted}
|
||||
isEdited={isEdited}
|
||||
getCell={getCell}
|
||||
isVisible={isVisible}
|
||||
disabled={formState.isDisabled}
|
||||
onGrant={accessPart.grant}
|
||||
onRevoke={accessPart.revoke}
|
||||
/>
|
||||
</ConnectionSectionWrapper>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
@@ -37,7 +37,8 @@ export class ConnectionFormAccessTabBootstrap extends Bootstrap {
|
||||
key: this.key,
|
||||
name: 'connections_connection_edit_access',
|
||||
title: 'connections_connection_edit_access',
|
||||
order: 4,
|
||||
icon: '/icons/plugin_connection_shield_user.svg',
|
||||
order: 6,
|
||||
stateGetter: context => () => getConnectionFormAccessPart(context.formState),
|
||||
getLoader: (_, context) => [
|
||||
getCachedMapResourceLoaderState(this.projectInfoResource, () => context?.formState.state.projectId ?? null),
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M10 6h10M10 12h10M10 18h10M4 5.5h2v2H4v-2ZM4 11h2v2H4v-2ZM4 16.5h2v2H4v-2Z" stroke="#6B7280" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 275 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="8.5" stroke="#6B7280" stroke-width="1.8"/>
|
||||
<path d="M3.75 12h16.5M12 3.5c2.15 2.32 3.25 5.15 3.25 8.5S14.15 18.18 12 20.5C9.85 18.18 8.75 15.35 8.75 12S9.85 5.82 12 3.5Z" stroke="#6B7280" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 390 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="m12 2.75 8 4.62v9.26l-8 4.62-8-4.62V7.37l8-4.62Z" stroke="#6B7280" stroke-width="1.8" stroke-linejoin="round"/>
|
||||
<circle cx="12" cy="12" r="3" stroke="#6B7280" stroke-width="1.8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 296 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="15.5" cy="8.5" r="5" stroke="#6B7280" stroke-width="1.8"/>
|
||||
<path d="m12 12-8.5 8.5M6 18l2 2M8.5 15.5l2 2" stroke="#6B7280" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 310 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<g transform="rotate(45 12 12)" stroke="#6B7280" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M8.5 3.5v5M15.5 3.5v5M6 8.5h12v2a6 6 0 0 1-12 0v-2ZM12 16.5v4"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 306 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M5 3v7M5 14v7M12 3v3M12 10v11M19 3v10M19 17v4M2.5 12h5M9.5 8h5M16.5 15h5" stroke="#6B7280" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 273 B |
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 { observer } from 'mobx-react-lite';
|
||||
import { useRef } from 'react';
|
||||
|
||||
import { Form, Placeholder, useFormValidator, useResource } from '@cloudbeaver/core-blocks';
|
||||
import { ConnectionInfoAuthPropertiesResource, DBDriverResource } from '@cloudbeaver/core-connections';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui';
|
||||
|
||||
import { ConnectionFormService } from '../ConnectionFormService.js';
|
||||
import { ConnectionSectionWrapper } from '../ConnectionSectionWrapper.js';
|
||||
import type { IConnectionFormProps } from '../IConnectionFormState.js';
|
||||
import { isConnectionFormReadOnly } from '../isConnectionFormReadOnly.js';
|
||||
import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js';
|
||||
import { ProviderPropertiesForm } from '../Options/ProviderPropertiesForm.js';
|
||||
|
||||
export const AdvancedSettings: TabContainerPanelComponent<IConnectionFormProps> = observer(function AdvancedSettings({ formState }) {
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const connectionFormService = useService(ConnectionFormService);
|
||||
const optionsPart = getConnectionFormOptionsPart(formState);
|
||||
const connectionInfoAuthResource = useResource(AdvancedSettings, ConnectionInfoAuthPropertiesResource, optionsPart.connectionKey);
|
||||
const driverResource = useResource(AdvancedSettings, DBDriverResource, {
|
||||
key: optionsPart.state.driverId || null,
|
||||
includes: ['includeProviderProperties'] as const,
|
||||
});
|
||||
const readonly = isConnectionFormReadOnly(formState, connectionInfoAuthResource.data?.authModel);
|
||||
|
||||
useFormValidator(formState.validationTask, formRef.current);
|
||||
|
||||
return (
|
||||
<Form ref={formRef} className="tw:flex tw:flex-1 tw:overflow-auto" disabled={driverResource.isLoading()}>
|
||||
<ConnectionSectionWrapper>
|
||||
<Placeholder container={connectionFormService.providerPropertiesContainer} formState={formState} />
|
||||
{driverResource.data?.providerProperties && (
|
||||
<ProviderPropertiesForm formState={formState} properties={driverResource.data.providerProperties} readonly={readonly} />
|
||||
)}
|
||||
</ConnectionSectionWrapper>
|
||||
</Form>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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 { importLazyComponent } from '@cloudbeaver/core-blocks';
|
||||
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
|
||||
import { ConnectionFormService } from '../ConnectionFormService.js';
|
||||
|
||||
const AdvancedSettings = importLazyComponent(() => import('./AdvancedSettings.js').then(m => m.AdvancedSettings));
|
||||
|
||||
@injectable(() => [ConnectionFormService])
|
||||
export class AdvancedSettingsTabService extends Bootstrap {
|
||||
constructor(private readonly connectionFormService: ConnectionFormService) {
|
||||
super();
|
||||
}
|
||||
|
||||
override register(): void {
|
||||
this.connectionFormService.parts.add({
|
||||
key: 'advanced_settings',
|
||||
name: 'plugin_connections_connection_form_part_advanced',
|
||||
icon: '/icons/plugin_connection_bulleted_list.svg',
|
||||
order: 9,
|
||||
panel: () => AdvancedSettings,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { Container, ObjectPropertyInfoForm, type ILayoutSizeProps } from '@cloudbeaver/core-blocks';
|
||||
import { ObjectPropertyInfoForm, type ILayoutSizeProps } from '@cloudbeaver/core-blocks';
|
||||
import { getObjectPropertyType, type IObjectPropertyInfo } from '@cloudbeaver/core-sdk';
|
||||
|
||||
interface Props {
|
||||
@@ -25,6 +25,10 @@ export const ConnectionAuthModelCredentialsForm = observer<Props>(function Conne
|
||||
readonly,
|
||||
disabled,
|
||||
}) {
|
||||
if (properties.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getLayoutSize(property: IObjectPropertyInfo): ILayoutSizeProps {
|
||||
const type = getObjectPropertyType(property);
|
||||
|
||||
@@ -38,19 +42,17 @@ export const ConnectionAuthModelCredentialsForm = observer<Props>(function Conne
|
||||
}
|
||||
|
||||
return (
|
||||
<Container wrap gap hideEmpty>
|
||||
<ObjectPropertyInfoForm
|
||||
autocompleteSectionName="section-connection-authentication"
|
||||
properties={properties}
|
||||
state={credentials}
|
||||
defaultState={defaultCredentials}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
getLayoutSize={getLayoutSize}
|
||||
autocompletePasswordType="new-password"
|
||||
showRememberTip
|
||||
hideEmptyPlaceholder
|
||||
/>
|
||||
</Container>
|
||||
<ObjectPropertyInfoForm
|
||||
autocompleteSectionName="section-connection-authentication"
|
||||
properties={properties}
|
||||
state={credentials}
|
||||
defaultState={defaultCredentials}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
getLayoutSize={getLayoutSize}
|
||||
autocompletePasswordType="new-password"
|
||||
showRememberTip
|
||||
hideEmptyPlaceholder
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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 { observer } from 'mobx-react-lite';
|
||||
import { useRef } from 'react';
|
||||
|
||||
import { Form, InputField, Placeholder, Textarea, useFormValidator, useResource, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import { ConnectionInfoAuthPropertiesResource } from '@cloudbeaver/core-connections';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { FormMode, type TabContainerPanelComponent } from '@cloudbeaver/core-ui';
|
||||
import { ProjectSelect } from '@cloudbeaver/plugin-projects';
|
||||
|
||||
import { ConnectionFormService } from '../ConnectionFormService.js';
|
||||
import type { IConnectionFormProps } from '../IConnectionFormState.js';
|
||||
import { ConnectionSectionWrapper } from '../ConnectionSectionWrapper.js';
|
||||
import { isConnectionFormReadOnly } from '../isConnectionFormReadOnly.js';
|
||||
import { ConnectionTypeForm } from '../Options/ConnectionTypeForm.js';
|
||||
import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js';
|
||||
|
||||
export const ConnectionDetails: TabContainerPanelComponent<IConnectionFormProps> = observer(function ConnectionDetails({ formState }) {
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const translate = useTranslate();
|
||||
const connectionFormService = useService(ConnectionFormService);
|
||||
const optionsPart = getConnectionFormOptionsPart(formState);
|
||||
const connectionInfoAuthResource = useResource(ConnectionDetails, ConnectionInfoAuthPropertiesResource, optionsPart.connectionKey);
|
||||
const readonly = isConnectionFormReadOnly(formState, connectionInfoAuthResource.data?.authModel);
|
||||
|
||||
useFormValidator(formState.validationTask, formRef.current);
|
||||
|
||||
function setProject(projectId: string) {
|
||||
formState.state.projectId = projectId;
|
||||
}
|
||||
|
||||
return (
|
||||
<Form ref={formRef} className="tw:flex tw:flex-1 tw:overflow-auto">
|
||||
<ConnectionSectionWrapper>
|
||||
<section className="tw:flex tw:min-w-0 tw:flex-col tw:gap-4">
|
||||
<div className="tw:grid tw:grid-cols-1 tw:gap-4">
|
||||
<InputField type="text" name="name" minLength={1} state={optionsPart.state} readOnly={readonly} required fill>
|
||||
{translate('connections_connection_name')}
|
||||
</InputField>
|
||||
<ProjectSelect
|
||||
value={formState.state.projectId}
|
||||
readOnly={readonly || formState.mode === FormMode.Edit}
|
||||
disabled={formState.isDisabled}
|
||||
autoHide
|
||||
onChange={setProject}
|
||||
/>
|
||||
<InputField type="text" name="folder" state={optionsPart.state} autoHide readOnly fill>
|
||||
{translate('plugin_connections_connection_form_part_main_folder')}
|
||||
</InputField>
|
||||
</div>
|
||||
<Textarea name="description" rows={3} state={optionsPart.state} readOnly={readonly}>
|
||||
{translate('connections_connection_description')}
|
||||
</Textarea>
|
||||
</section>
|
||||
|
||||
<section className="theme-border-color-background tw:flex tw:min-w-0 tw:flex-col tw:gap-6 tw:border-t tw:pt-6">
|
||||
<ConnectionTypeForm config={optionsPart.state} />
|
||||
<Placeholder container={connectionFormService.connectionContainer} formState={formState} />
|
||||
</section>
|
||||
</ConnectionSectionWrapper>
|
||||
</Form>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
|
||||
import { Translate } from '@cloudbeaver/core-blocks';
|
||||
import { Tab, type TabContainerTabComponent, TabIcon, TabTitle } from '@cloudbeaver/core-ui';
|
||||
|
||||
import type { IConnectionFormProps } from '../IConnectionFormState.js';
|
||||
|
||||
export const ConnectionDetailsTab: TabContainerTabComponent<IConnectionFormProps> = function ConnectionDetailsTab(props) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="theme-border-color-background tw:mx-2 tw:my-2 tw:shrink-0 tw:border-t" aria-hidden />
|
||||
<Tab {...props}>
|
||||
<TabIcon icon="/icons/plugin_connection_key.svg" viewBox="0 0 16 16" />
|
||||
<TabTitle>
|
||||
<Translate token="plugin_connections_connection_form_connection_details" />
|
||||
</TabTitle>
|
||||
</Tab>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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 { importLazyComponent } from '@cloudbeaver/core-blocks';
|
||||
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
|
||||
import { ConnectionFormService } from '../ConnectionFormService.js';
|
||||
|
||||
const ConnectionDetails = importLazyComponent(() => import('./ConnectionDetails.js').then(m => m.ConnectionDetails));
|
||||
const ConnectionDetailsTab = importLazyComponent(() => import('./ConnectionDetailsTab.js').then(m => m.ConnectionDetailsTab));
|
||||
|
||||
@injectable(() => [ConnectionFormService])
|
||||
export class ConnectionDetailsTabService extends Bootstrap {
|
||||
constructor(private readonly connectionFormService: ConnectionFormService) {
|
||||
super();
|
||||
}
|
||||
|
||||
override register(): void {
|
||||
this.connectionFormService.parts.add({
|
||||
key: 'connection_details',
|
||||
name: 'plugin_connections_connection_form_connection_details',
|
||||
icon: '/icons/plugin_connection_key.svg',
|
||||
order: 8,
|
||||
tab: () => ConnectionDetailsTab,
|
||||
panel: () => ConnectionDetails,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,43 +1,88 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 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.
|
||||
*/
|
||||
|
||||
.tabList {
|
||||
composes: theme-background-surface theme-text-on-surface theme-border-color-background from global;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
gap: 2px;
|
||||
width: 176px;
|
||||
min-width: 176px;
|
||||
height: 100%;
|
||||
padding: 16px 8px;
|
||||
border-right: 1px solid;
|
||||
}
|
||||
|
||||
.tab {
|
||||
composes: theme-ripple from global;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
color: inherit;
|
||||
|
||||
&:global([aria-selected='true']) {
|
||||
background-color: color-mix(in srgb, transparent, var(--theme-primary) 18%);
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.tab:global([role='tab']) {
|
||||
height: auto;
|
||||
min-height: 28px;
|
||||
max-height: 48px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.tabTitle {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
max-height: 40px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
line-height: 20px;
|
||||
white-space: normal;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.connectionTopBar {
|
||||
composes: theme-border-color-background theme-background-secondary theme-text-on-secondary from global;
|
||||
composes: theme-border-color-background theme-background-surface theme-text-on-surface from global;
|
||||
}
|
||||
|
||||
.connectionTopBar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding-top: 16px;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
border-bottom: solid 2px;
|
||||
border-color: inherit;
|
||||
}
|
||||
flex-direction: column;
|
||||
min-height: 64px;
|
||||
padding: 8px 0 16px;
|
||||
border-bottom: 1px solid;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.connectionTopBarTabs {
|
||||
flex: 1;
|
||||
.connectionTopBarContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.connectionTopBarStatus {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.connectionTopBarActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
padding: 0 24px;
|
||||
gap: 16px;
|
||||
}
|
||||
@@ -45,7 +90,7 @@
|
||||
.connectionStatusMessage {
|
||||
composes: theme-typography--caption from global;
|
||||
height: 24px;
|
||||
padding: 0 16px;
|
||||
padding: 0 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
@@ -57,19 +102,29 @@
|
||||
}
|
||||
|
||||
.box {
|
||||
composes: theme-background-secondary theme-text-on-secondary from global;
|
||||
composes: theme-background-surface theme-text-on-surface from global;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.connectionBody {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.contentBox {
|
||||
composes: theme-background-secondary theme-border-color-background from global;
|
||||
composes: theme-background-surface theme-text-on-surface theme-border-color-background from global;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -7,14 +7,37 @@
|
||||
*/
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { Container, Form, Loader, Placeholder, s, StatusMessage, useForm, useObjectRef, useS } from '@cloudbeaver/core-blocks';
|
||||
import {
|
||||
Container,
|
||||
Form,
|
||||
Loader,
|
||||
Placeholder,
|
||||
s,
|
||||
SContext,
|
||||
StatusMessage,
|
||||
type StyleRegistry,
|
||||
useForm,
|
||||
useObjectRef,
|
||||
useS,
|
||||
} from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { ENotificationType, NotificationService } from '@cloudbeaver/core-events';
|
||||
import type { ConnectionConfig } from '@cloudbeaver/core-sdk';
|
||||
import { FormMode, formSubmitContext, TabList, TabPanelList, TabsState, type IFormState } from '@cloudbeaver/core-ui';
|
||||
import {
|
||||
FormMode,
|
||||
formSubmitContext,
|
||||
TabList,
|
||||
TabListStyles,
|
||||
TabPanelList,
|
||||
TabsState,
|
||||
TabStyles,
|
||||
TabTitleStyles,
|
||||
type IFormState,
|
||||
} from '@cloudbeaver/core-ui';
|
||||
|
||||
import { ConnectionFormActionsContext, type IConnectionFormActionsContext } from './ConnectFormActionsContext.js';
|
||||
import style from './ConnectionForm.module.css';
|
||||
import { ConnectionName } from './ConnectionName.js';
|
||||
import type { ConnectionFormState } from './ConnectionFormState.js';
|
||||
import { getFirstException } from '@cloudbeaver/core-utils';
|
||||
import { ConnectionFormService } from './ConnectionFormService.js';
|
||||
@@ -22,14 +45,20 @@ import { getConnectionFormOptionsPart } from './Options/getConnectionFormOptions
|
||||
import { ExecutionContext } from '@cloudbeaver/core-executor';
|
||||
import type { IConnectionFormState } from './IConnectionFormState.js';
|
||||
|
||||
export interface ConnectionFormProps {
|
||||
export interface IConnectionFormComponentProps {
|
||||
formState: ConnectionFormState;
|
||||
onCancel?: () => void;
|
||||
onSave?: (config: ConnectionConfig, isCreate?: boolean) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ConnectionForm = observer<ConnectionFormProps>(function ConnectionForm({ formState, onCancel, onSave = () => {}, className }) {
|
||||
const tabsRegistry: StyleRegistry = [
|
||||
[TabStyles, { mode: 'append', styles: [style] }],
|
||||
[TabListStyles, { mode: 'append', styles: [style] }],
|
||||
[TabTitleStyles, { mode: 'append', styles: [style] }],
|
||||
];
|
||||
|
||||
export const ConnectionForm = observer<IConnectionFormComponentProps>(function ConnectionForm({ formState, onCancel, onSave = () => {}, className }) {
|
||||
const service = useService(ConnectionFormService);
|
||||
const styles = useS(style);
|
||||
const notificationService = useService(NotificationService);
|
||||
@@ -79,10 +108,10 @@ export const ConnectionForm = observer<ConnectionFormProps>(function ConnectionF
|
||||
|
||||
return (
|
||||
<Form context={form} contents>
|
||||
<TabsState container={service.parts} localState={formState.parts} formState={formState}>
|
||||
<TabsState container={service.parts} localState={formState.parts} orientation="vertical" formState={formState}>
|
||||
<div className={s(styles, { box: true }, className)}>
|
||||
<div className={s(styles, { connectionTopBar: true })}>
|
||||
<Container className={s(styles, { connectionTopBarTabs: true })} overflow>
|
||||
<Container className={s(styles, { connectionTopBarStatus: true })} overflow>
|
||||
<Container className={s(styles, { connectionStatusMessage: true })} overflow>
|
||||
<StatusMessage
|
||||
type={exception ? ENotificationType.Error : ENotificationType.Info}
|
||||
@@ -90,18 +119,25 @@ export const ConnectionForm = observer<ConnectionFormProps>(function ConnectionF
|
||||
exception={exception}
|
||||
/>
|
||||
</Container>
|
||||
<TabList className={s(styles, { tabList: true })} disabled={formState.isDisabled} underline big />
|
||||
</Container>
|
||||
<div className={s(styles, { connectionTopBarActions: true })}>
|
||||
<Loader suspense inline hideMessage hideException>
|
||||
<ConnectionFormActionsContext.Provider value={actionsContext}>
|
||||
<Placeholder container={service.actionsContainer} formState={formState} />
|
||||
</ConnectionFormActionsContext.Provider>
|
||||
</Loader>
|
||||
<div className={s(styles, { connectionTopBarContent: true })}>
|
||||
{optionsPart.state.name && <ConnectionName driverId={optionsPart.state.driverId} name={optionsPart.state.name} />}
|
||||
<div className={s(styles, { connectionTopBarActions: true })}>
|
||||
<Loader suspense inline hideMessage hideException>
|
||||
<ConnectionFormActionsContext.Provider value={actionsContext}>
|
||||
<Placeholder container={service.actionsContainer} formState={formState} />
|
||||
</ConnectionFormActionsContext.Provider>
|
||||
</Loader>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={s(styles, { contentBox: true })}>
|
||||
<TabPanelList />
|
||||
<div className={s(styles, { connectionBody: true })}>
|
||||
<SContext registry={tabsRegistry}>
|
||||
<TabList className={s(styles, { tabList: true })} disabled={formState.isDisabled} vertical />
|
||||
</SContext>
|
||||
<div className={s(styles, { contentBox: true })}>
|
||||
<TabPanelList />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsState>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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 { observer } from 'mobx-react-lite';
|
||||
|
||||
import { IconOrImage, useResource } from '@cloudbeaver/core-blocks';
|
||||
import { DBDriverResource } from '@cloudbeaver/core-connections';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
driverId?: string;
|
||||
}
|
||||
|
||||
export const ConnectionName = observer<Props>(function ConnectionName({ driverId, name }) {
|
||||
const driverResource = useResource(ConnectionName, DBDriverResource, driverId || null);
|
||||
const driver = driverResource.data;
|
||||
|
||||
return (
|
||||
<div className="tw:ml-6 tw:flex tw:max-w-60 tw:min-w-0 tw:items-center tw:gap-2" title={name}>
|
||||
{driver?.icon && <IconOrImage className="tw:size-5 tw:shrink-0" icon={driver.icon} />}
|
||||
<span className="tw:truncate tw:text-sm tw:font-medium">{name}</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 { observer } from 'mobx-react-lite';
|
||||
|
||||
import { clsx } from '@dbeaver/ui-kit';
|
||||
|
||||
export interface IConnectionSectionWrapperProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ConnectionSectionWrapper = observer<React.PropsWithChildren<IConnectionSectionWrapperProps>>(function ConnectionSectionWrapper({
|
||||
children,
|
||||
className,
|
||||
}) {
|
||||
return <div className={clsx('tw:flex tw:w-full tw:max-w-xl tw:flex-col tw:gap-6 tw:p-6', className)}>{children}</div>;
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* 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 { importLazyComponent } from '@cloudbeaver/core-blocks';
|
||||
|
||||
export const ConnectionSectionWrapper = importLazyComponent(() =>
|
||||
import('./ConnectionSectionWrapper.js').then(m => m.ConnectionSectionWrapper),
|
||||
);
|
||||
@@ -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.
|
||||
@@ -24,6 +24,7 @@ export class ConnectionDriverPropertiesTabService extends Bootstrap {
|
||||
key: 'driver_properties',
|
||||
name: 'plugin_connections_connection_form_part_properties',
|
||||
title: 'plugin_connections_connection_form_part_properties',
|
||||
icon: '/icons/plugin_connection_hex_nut.svg',
|
||||
order: 2,
|
||||
panel: () => DriverProperties,
|
||||
isDisabled: (tabId, props) => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ConnectionInfoPropertiesResource, ConnectionInfoResource } from '@cloud
|
||||
import type { IConnectionFormState } from '../IConnectionFormState.js';
|
||||
import { runInAction, toJS } from 'mobx';
|
||||
import type { ConnectionFormOptionsPart } from '../Options/ConnectionFormOptionsPart.js';
|
||||
import type { schema } from '@cloudbeaver/core-utils';
|
||||
import { type schema, trimObjectValues } from '@cloudbeaver/core-utils';
|
||||
import { getObjectPropertyDefaultValue, getObjectPropertyOptionValue, getObjectPropertyValue } from '@cloudbeaver/core-sdk';
|
||||
import type { CONNECTION_PROPERTIES_SCHEMA } from '../CONNECTION_CONFIG_SCHEMA.js';
|
||||
|
||||
@@ -74,18 +74,14 @@ export class ConnectionFormDriverPropertiesPart extends FormPart<ConnectionPrope
|
||||
contexts: IExecutionContextProvider<IFormState<IConnectionFormState>>,
|
||||
): Promise<void> {}
|
||||
|
||||
protected override async format(
|
||||
data: IFormState<IConnectionFormState>,
|
||||
contexts: IExecutionContextProvider<IFormState<IConnectionFormState>>,
|
||||
): Promise<void> {
|
||||
protected override format(): void {
|
||||
runInAction(() => {
|
||||
for (const key of Object.keys(this.state!)) {
|
||||
if (typeof this.state[key] === 'string') {
|
||||
this.state[key] = this.state[key].trim();
|
||||
}
|
||||
}
|
||||
trimObjectValues(this.state);
|
||||
trimObjectValues(this.optionsPart.state.properties);
|
||||
});
|
||||
}
|
||||
|
||||
protected override async prepare(): Promise<void> {
|
||||
this.optionsPart.state.properties = await this.getPropertiesConfig();
|
||||
}
|
||||
|
||||
@@ -98,9 +94,10 @@ export class ConnectionFormDriverPropertiesPart extends FormPart<ConnectionPrope
|
||||
|
||||
const properties = await this.connectionInfoResource.getConnectionDriverProperties(this.formState.state.projectId, this.optionsPart.state);
|
||||
|
||||
/* Default property values must not be returned. If they are included in the request, the backend will send them back with modified values (e.g., null converted to an empty string).
|
||||
To avoid this behavior, only properties that were explicitly changed should be sent. Any properties that still contain default values must be removed from the object before sending the request
|
||||
*/
|
||||
/*
|
||||
* Default property values must not be returned. If they are included in the request, the backend will send them back with
|
||||
* modified values (e.g., null converted to an empty string). Only explicitly changed properties should be sent.
|
||||
*/
|
||||
for (const [key, value] of Object.entries(config)) {
|
||||
const property = properties?.find(property => property.id === key);
|
||||
if (property && value === getObjectPropertyOptionValue(property.defaultValue)) {
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 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.
|
||||
*/
|
||||
|
||||
.coloredContainer {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.group {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.propertiesTable {
|
||||
padding-top: 8px;
|
||||
composes: theme-border-color-background from global;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid;
|
||||
border-radius: 8px;
|
||||
padding-block: 8px;
|
||||
}
|
||||
|
||||
.propertiesList {
|
||||
|
||||
@@ -10,8 +10,6 @@ import { observer } from 'mobx-react-lite';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
ColoredContainer,
|
||||
Group,
|
||||
type IProperty,
|
||||
PropertiesTable,
|
||||
PropertiesTableStyles,
|
||||
@@ -26,6 +24,7 @@ import { type TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui';
|
||||
import { uuid } from '@cloudbeaver/core-utils';
|
||||
|
||||
import styles from './DriverProperties.module.css';
|
||||
import { ConnectionSectionWrapper } from '../ConnectionSectionWrapper.js';
|
||||
import { getConnectionFormDriverPropertiesPart } from './getConnectionFormDriverPropertiesPart.js';
|
||||
import type { IConnectionFormProps } from '../IConnectionFormState.js';
|
||||
import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js';
|
||||
@@ -110,8 +109,8 @@ export const DriverProperties: TabContainerPanelComponent<IConnectionFormProps>
|
||||
useAutoLoad(DriverProperties, propertiesState, selected, undefined, true);
|
||||
|
||||
return (
|
||||
<ColoredContainer className={s(style, { coloredContainer: true })} parent>
|
||||
<Group className={s(style, { group: true })} box large>
|
||||
<div className="tw:flex tw:flex-1 tw:overflow-auto">
|
||||
<ConnectionSectionWrapper className="tw:max-w-3xl!">
|
||||
<SContext registry={registry}>
|
||||
<PropertiesTable
|
||||
className={s(style, { propertiesTable: true })}
|
||||
@@ -123,7 +122,7 @@ export const DriverProperties: TabContainerPanelComponent<IConnectionFormProps>
|
||||
onRemove={state.remove}
|
||||
/>
|
||||
</SContext>
|
||||
</Group>
|
||||
</ColoredContainer>
|
||||
</ConnectionSectionWrapper>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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 { observer } from 'mobx-react-lite';
|
||||
import { useRef } from 'react';
|
||||
|
||||
import { Form, useFormValidator, useResource } from '@cloudbeaver/core-blocks';
|
||||
import { ConnectionInfoAuthPropertiesResource, DBDriverExpertSettingsResource } from '@cloudbeaver/core-connections';
|
||||
import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui';
|
||||
|
||||
import { ConnectionSectionWrapper } from '../ConnectionSectionWrapper.js';
|
||||
import type { IConnectionFormProps } from '../IConnectionFormState.js';
|
||||
import { isConnectionFormReadOnly } from '../isConnectionFormReadOnly.js';
|
||||
import { AdvancedPropertiesForm } from '../Options/AdvancedPropertiesForm.js';
|
||||
import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js';
|
||||
|
||||
export const InitializationSettings: TabContainerPanelComponent<IConnectionFormProps> = observer(function InitializationSettings({ formState }) {
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const optionsPart = getConnectionFormOptionsPart(formState);
|
||||
const connectionInfoAuthResource = useResource(InitializationSettings, ConnectionInfoAuthPropertiesResource, optionsPart.connectionKey);
|
||||
const propertiesResource = useResource(InitializationSettings, DBDriverExpertSettingsResource, optionsPart.state.driverId ?? null);
|
||||
const readonly = isConnectionFormReadOnly(formState, connectionInfoAuthResource.data?.authModel);
|
||||
|
||||
useFormValidator(formState.validationTask, formRef.current);
|
||||
|
||||
return (
|
||||
<Form ref={formRef} className="tw:flex tw:flex-1 tw:overflow-auto" disabled={propertiesResource.isLoading()}>
|
||||
<ConnectionSectionWrapper>
|
||||
<AdvancedPropertiesForm config={optionsPart.state} disabled={formState.isDisabled} readonly={readonly} />
|
||||
</ConnectionSectionWrapper>
|
||||
</Form>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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 { importLazyComponent } from '@cloudbeaver/core-blocks';
|
||||
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
|
||||
import { ConnectionFormService } from '../ConnectionFormService.js';
|
||||
|
||||
const InitializationSettings = importLazyComponent(() => import('./InitializationSettings.js').then(m => m.InitializationSettings));
|
||||
|
||||
@injectable(() => [ConnectionFormService])
|
||||
export class InitializationSettingsTabService extends Bootstrap {
|
||||
constructor(private readonly connectionFormService: ConnectionFormService) {
|
||||
super();
|
||||
}
|
||||
|
||||
override register(): void {
|
||||
this.connectionFormService.parts.add({
|
||||
key: 'initialization_settings',
|
||||
name: 'plugin_connections_connection_form_part_initialization_settings',
|
||||
icon: '/icons/plugin_connection_vertical_sliders.svg',
|
||||
order: 10,
|
||||
panel: () => InitializationSettings,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { Container, Expandable, Group, ObjectPropertyInfoForm, useResource, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import { ObjectPropertyInfoForm, useResource } from '@cloudbeaver/core-blocks';
|
||||
import type { ConnectionConfig } from '@cloudbeaver/core-sdk';
|
||||
import { DBDriverExpertSettingsResource } from '@cloudbeaver/core-connections';
|
||||
|
||||
@@ -18,7 +18,6 @@ interface Props {
|
||||
}
|
||||
|
||||
export const AdvancedPropertiesForm = observer<Props>(function AdvancedPropertiesForm({ config, disabled, readonly }) {
|
||||
const translate = useTranslate();
|
||||
const properties = useResource(AdvancedPropertiesForm, DBDriverExpertSettingsResource, config.driverId ?? null);
|
||||
|
||||
if (!properties.data?.length) {
|
||||
@@ -26,12 +25,8 @@ export const AdvancedPropertiesForm = observer<Props>(function AdvancedPropertie
|
||||
}
|
||||
|
||||
return (
|
||||
<Group form gap>
|
||||
<Expandable label={translate('connections_connection_expert_settings')}>
|
||||
<Container wrap gap>
|
||||
<ObjectPropertyInfoForm state={config.expertSettingsValues} properties={properties.data} disabled={disabled} readOnly={readonly} />
|
||||
</Container>
|
||||
</Expandable>
|
||||
</Group>
|
||||
<div className="tw:flex tw:flex-col tw:gap-4">
|
||||
<ObjectPropertyInfoForm state={config.expertSettingsValues} properties={properties.data} disabled={disabled} readOnly={readonly} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@ import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dial
|
||||
import { LocalizationService } from '@cloudbeaver/core-localization';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import { action, computed, makeObservable, observable, reaction, toJS } from 'mobx';
|
||||
import { getUniqueName } from '@cloudbeaver/core-utils';
|
||||
import { getUniqueName, trimObjectValues } from '@cloudbeaver/core-utils';
|
||||
import { getObjectPropertyDefaults } from '@cloudbeaver/core-blocks';
|
||||
import { isNotNullDefined } from '@dbeaver/js-helpers';
|
||||
import { parseJdbcUri } from '@dbeaver/jdbc-uri-parser';
|
||||
@@ -351,7 +351,7 @@ export class ConnectionFormOptionsPart extends FormPart<IConnectionFormOptionsSt
|
||||
return config;
|
||||
}
|
||||
|
||||
protected override async format(
|
||||
protected override async prepare(
|
||||
data: IFormState<IConnectionFormState>,
|
||||
contexts: IExecutionContextProvider<IFormState<IConnectionFormState>>,
|
||||
): Promise<void> {
|
||||
@@ -367,31 +367,26 @@ export class ConnectionFormOptionsPart extends FormPart<IConnectionFormOptionsSt
|
||||
this.formState.state.requiredNetworkHandlersIds = observable([]);
|
||||
this.state.networkHandlersConfig = observable([]);
|
||||
|
||||
this.state.name = this.state.name?.trim();
|
||||
this.state.description = this.state.description?.trim();
|
||||
|
||||
if (!this.state.folder) {
|
||||
delete this.state.folder;
|
||||
}
|
||||
|
||||
if (this.state.configurationType === DriverConfigurationType.Url) {
|
||||
this.state.url = this.state.url?.trim();
|
||||
} else {
|
||||
if (this.state.configurationType !== DriverConfigurationType.Url) {
|
||||
// if manual type configuration set, it helps to keep host, port, etc. properties (not saved on backend)
|
||||
delete this.state.url;
|
||||
}
|
||||
|
||||
// databaseName, host, port, serverName only saves on backend like this
|
||||
if (this.state.configurationType === DriverConfigurationType.Manual && !driver.useCustomPage) {
|
||||
this.state.mainPropertyValues![MAIN_PROPERTY_DATABASE_KEY] = this.state.databaseName?.trim();
|
||||
this.state.mainPropertyValues![MAIN_PROPERTY_DATABASE_KEY] = this.state.databaseName;
|
||||
|
||||
if (!driver.embedded) {
|
||||
this.state.mainPropertyValues![MAIN_PROPERTY_HOST_KEY] = this.state.host?.trim();
|
||||
this.state.mainPropertyValues![MAIN_PROPERTY_PORT_KEY] = this.state.port?.trim();
|
||||
this.state.mainPropertyValues![MAIN_PROPERTY_HOST_KEY] = this.state.host;
|
||||
this.state.mainPropertyValues![MAIN_PROPERTY_PORT_KEY] = this.state.port;
|
||||
}
|
||||
|
||||
if (driver.requiresServerName) {
|
||||
this.state.mainPropertyValues![MAIN_PROPERTY_SERVER_KEY] = this.state.serverName?.trim();
|
||||
this.state.mainPropertyValues![MAIN_PROPERTY_SERVER_KEY] = this.state.serverName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,6 +435,18 @@ export class ConnectionFormOptionsPart extends FormPart<IConnectionFormOptionsSt
|
||||
}
|
||||
}
|
||||
|
||||
protected override format(): void {
|
||||
this.state.name = this.state.name?.trim();
|
||||
this.state.description = this.state.description?.trim();
|
||||
this.state.url = this.state.url?.trim();
|
||||
|
||||
trimObjectValues(this.state.credentials);
|
||||
trimObjectValues(this.state.properties);
|
||||
trimObjectValues(this.state.providerProperties);
|
||||
trimObjectValues(this.state.mainPropertyValues);
|
||||
trimObjectValues(this.state.expertSettingsValues);
|
||||
}
|
||||
|
||||
private async getConnectionAuthModelProperties(authModelId: string, connectionInfo?: ConnectionInfoAuthProperties): Promise<IObjectPropertyInfo[]> {
|
||||
const authModel = await this.databaseAuthModelsResource.load(authModelId);
|
||||
|
||||
@@ -587,12 +594,6 @@ function prepareDynamicProperties(
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of Object.keys(result)) {
|
||||
if (typeof result[key] === 'string') {
|
||||
result[key] = result[key]?.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -26,6 +26,7 @@ export class ConnectionOptionsTabService extends Bootstrap {
|
||||
this.connectionFormService.parts.add({
|
||||
key: 'options',
|
||||
name: 'plugin_connections_connection_form_part_main',
|
||||
icon: '/icons/plugin_connection_plug.svg',
|
||||
order: 1,
|
||||
panel: () => Options,
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { Container, Group, GroupTitle, Select, useResource, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import { GroupTitle, Select, useResource, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import { CachedMapAllKey } from '@cloudbeaver/core-resource';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { isNotNullDefined } from '@dbeaver/js-helpers';
|
||||
@@ -28,21 +28,19 @@ export const ConnectionTypeForm = observer<Props>(function ConnectionTypeForm({
|
||||
const types = connectionTypeResource.data.filter(isNotNullDefined);
|
||||
|
||||
return (
|
||||
<Group form gap>
|
||||
<div className="tw:flex tw:flex-col tw:gap-2">
|
||||
<GroupTitle>{translate('plugin_connections_connection_type_label')}</GroupTitle>
|
||||
<Container gap dense>
|
||||
<Select
|
||||
items={types}
|
||||
name="connectionType"
|
||||
state={config}
|
||||
keySelector={t => t.id}
|
||||
valueSelector={t => (t.id === DEFAULT_TYPE_ID ? `${t.name} (${translate('ui_default')})` : t.name)}
|
||||
iconSelector={t => {
|
||||
const color = connectionTypeService.getTypeColor(t.id);
|
||||
return <ColorIndicator className="tw:w-3.5! tw:h-3.5!" color={color ?? MAPPED_PREDEFINED_UNSET_COLOR} />;
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
</Group>
|
||||
<Select
|
||||
items={types}
|
||||
name="connectionType"
|
||||
state={config}
|
||||
keySelector={t => t.id}
|
||||
valueSelector={t => (t.id === DEFAULT_TYPE_ID ? `${t.name} (${translate('ui_default')})` : t.name)}
|
||||
iconSelector={t => {
|
||||
const color = connectionTypeService.getTypeColor(t.id);
|
||||
return <ColorIndicator className="tw:w-3.5! tw:h-3.5!" color={color ?? MAPPED_PREDEFINED_UNSET_COLOR} />;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
.form {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
@@ -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.
|
||||
@@ -11,32 +11,25 @@ import { useContext, useRef } from 'react';
|
||||
import { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication';
|
||||
import {
|
||||
Alert,
|
||||
ColoredContainer,
|
||||
Combobox,
|
||||
Container,
|
||||
FieldCheckbox,
|
||||
Form,
|
||||
FormFieldDescription,
|
||||
getComputed,
|
||||
Group,
|
||||
GroupTitle,
|
||||
InputField,
|
||||
Link,
|
||||
ObjectPropertyInfoForm,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
s,
|
||||
Textarea,
|
||||
useAdministrationSettings,
|
||||
useFormValidator,
|
||||
usePermission,
|
||||
useResource,
|
||||
useS,
|
||||
useTranslate,
|
||||
useAuthenticationAction,
|
||||
useAutoLoad,
|
||||
Text,
|
||||
Placeholder,
|
||||
GroupTitle,
|
||||
} from '@cloudbeaver/core-blocks';
|
||||
import {
|
||||
ConnectionInfoAuthPropertiesResource,
|
||||
@@ -52,21 +45,15 @@ import { EAdminPermission, ServerConfigResource } from '@cloudbeaver/core-root';
|
||||
import { DriverConfigurationType } from '@cloudbeaver/core-sdk';
|
||||
import { type TabContainerPanelComponent, TabsContext } from '@cloudbeaver/core-ui';
|
||||
import { EMPTY_ARRAY } from '@cloudbeaver/core-utils';
|
||||
import { ProjectSelect } from '@cloudbeaver/plugin-projects';
|
||||
|
||||
import { ConnectionAuthModelCredentialsForm } from '../ConnectionAuthModelCredentials/ConnectionAuthModelCredentialsForm.js';
|
||||
import { ConnectionAuthModelSelector } from '../ConnectionAuthModelCredentials/ConnectionAuthModelSelector.js';
|
||||
import { ConnectionSectionWrapper } from '../ConnectionSectionWrapper.js';
|
||||
import { isConnectionFormReadOnly } from '../isConnectionFormReadOnly.js';
|
||||
import { CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID } from '../SharedCredentials/CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID.js';
|
||||
import { AdvancedPropertiesForm } from './AdvancedPropertiesForm.js';
|
||||
import styles from './Options.module.css';
|
||||
import { ParametersForm } from './ParametersForm.js';
|
||||
import { ProviderPropertiesForm } from './ProviderPropertiesForm.js';
|
||||
import { getConnectionFormOptionsPart } from './getConnectionFormOptionsPart.js';
|
||||
import type { IConnectionFormProps } from '../IConnectionFormState.js';
|
||||
import { ConnectionFormService } from '../ConnectionFormService.js';
|
||||
import { ConnectionTypeForm } from './ConnectionTypeForm.js';
|
||||
|
||||
const PROFILE_AUTH_MODEL_ID = 'profile';
|
||||
|
||||
interface IDriverConfiguration {
|
||||
name: string;
|
||||
@@ -89,13 +76,12 @@ const driverConfiguration: IDriverConfiguration[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const Options: TabContainerPanelComponent<IConnectionFormProps> = observer(function Options({ formState, tabId }) {
|
||||
export const Options: TabContainerPanelComponent<IConnectionFormProps> = observer(function Options({ formState }) {
|
||||
const isAdmin = usePermission(EAdminPermission.admin);
|
||||
const serverConfigResource = useResource(Options, ServerConfigResource, undefined);
|
||||
const projectInfoResource = useService(ProjectInfoResource);
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const translate = useTranslate();
|
||||
const style = useS(styles);
|
||||
const tabsState = useContext(TabsContext);
|
||||
const isSharedProject = projectInfoResource.isProjectShared(formState.state.projectId);
|
||||
const optionsPart = getConnectionFormOptionsPart(formState);
|
||||
@@ -103,10 +89,8 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
|
||||
const connectionInfoOriginResource = useResource(Options, ConnectionInfoOriginResource, optionsPart.connectionKey);
|
||||
const connectionInfoAuthPropertiesResource = useResource(Options, ConnectionInfoAuthPropertiesResource, optionsPart.connectionKey);
|
||||
const configurationTypeLabel = translate('connections_connection_configuration');
|
||||
const connectionFormService = useService(ConnectionFormService);
|
||||
|
||||
//@TODO it's here until the profile implementation in the CloudBeaver
|
||||
const readonly = formState.isDisabled || formState.isReadOnly || connectionInfoAuthResource.data?.authModel === PROFILE_AUTH_MODEL_ID;
|
||||
const readonly = isConnectionFormReadOnly(formState, connectionInfoAuthResource.data?.authModel);
|
||||
|
||||
useFormValidator(formState.validationTask, formRef.current);
|
||||
const { credentialsSavingEnabled } = useAdministrationSettings();
|
||||
@@ -149,10 +133,6 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
|
||||
return formState.state.availableDrivers.includes(id);
|
||||
});
|
||||
|
||||
function setProject(projectId: string) {
|
||||
formState.state.projectId = projectId;
|
||||
}
|
||||
|
||||
let properties = authModel?.properties;
|
||||
|
||||
if (
|
||||
@@ -164,6 +144,7 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
|
||||
}
|
||||
|
||||
const sharedCredentials = optionsPart.state.sharedCredentials && serverConfigResource.data?.distributed;
|
||||
const authenticationVisible = !driver?.anonymousAccess && (authentication.authorized || !edit);
|
||||
|
||||
function openCredentialsTab(event: React.MouseEvent<HTMLAnchorElement>) {
|
||||
event.preventDefault();
|
||||
@@ -177,90 +158,85 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
|
||||
useAutoLoad(Options, optionsPart);
|
||||
|
||||
return (
|
||||
<Form ref={formRef} className={s(style, { form: true })} disabled={driverMap.isLoading()}>
|
||||
<ColoredContainer wrap overflow parent gap>
|
||||
<Container medium gap>
|
||||
<Group gap>
|
||||
{isAdmin && !driver?.driverInstalled && (
|
||||
<Alert title={translate('core_connections_connection_driver_not_installed')}>
|
||||
<Text>{translate('plugin_connections_connection_driver_not_installed_message')}</Text>
|
||||
</Alert>
|
||||
<Form ref={formRef} className="tw:flex tw:flex-1 tw:overflow-auto" disabled={driverMap.isLoading()}>
|
||||
<ConnectionSectionWrapper>
|
||||
<section className="tw:flex tw:min-w-0 tw:flex-col tw:gap-4">
|
||||
{isAdmin && !driver?.driverInstalled && (
|
||||
<Alert title={translate('core_connections_connection_driver_not_installed')}>
|
||||
<Text>{translate('plugin_connections_connection_driver_not_installed_message')}</Text>
|
||||
</Alert>
|
||||
)}
|
||||
<div className="tw:grid tw:grid-cols-1 tw:gap-4 tw:lg:grid-cols-2">
|
||||
<Combobox
|
||||
name="driverId"
|
||||
value={optionsPart.state.driverId}
|
||||
items={drivers}
|
||||
keySelector={driver => driver.id}
|
||||
valueSelector={driver => driver.name ?? ''}
|
||||
titleSelector={driver => driver.description}
|
||||
iconSelector={driver => driver.icon}
|
||||
readOnly={readonly || edit || drivers.length < 2}
|
||||
disabled={formState.isDisabled}
|
||||
loading={driverMap.isLoading()}
|
||||
tiny
|
||||
fill
|
||||
onSelect={setDriverIdHandler}
|
||||
>
|
||||
{translate('connections_connection_driver')}
|
||||
</Combobox>
|
||||
{configurationTypes.length > 1 && (
|
||||
<FormFieldDescription label={configurationTypeLabel} tiny>
|
||||
<RadioGroup aria-label={configurationTypeLabel} name="configurationType" state={optionsPart.state}>
|
||||
{configurationTypes.map(conf => (
|
||||
<Radio key={conf.value} id={conf.value} value={conf.value} disabled={readonly} small keepSize>
|
||||
{conf.name}
|
||||
</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormFieldDescription>
|
||||
)}
|
||||
<Group form box gap>
|
||||
<Container wrap gap>
|
||||
<Combobox
|
||||
name="driverId"
|
||||
value={optionsPart.state.driverId}
|
||||
items={drivers}
|
||||
keySelector={driver => driver.id}
|
||||
valueSelector={driver => driver.name ?? ''}
|
||||
titleSelector={driver => driver.description}
|
||||
iconSelector={driver => driver.icon}
|
||||
readOnly={readonly || edit || drivers.length < 2}
|
||||
disabled={formState.isDisabled}
|
||||
loading={driverMap.isLoading()}
|
||||
tiny
|
||||
fill
|
||||
onSelect={setDriverIdHandler}
|
||||
>
|
||||
{translate('connections_connection_driver')}
|
||||
</Combobox>
|
||||
{configurationTypes.length > 1 && (
|
||||
<FormFieldDescription label={configurationTypeLabel} tiny>
|
||||
<Container gap>
|
||||
<RadioGroup aria-label={configurationTypeLabel} name="configurationType" state={optionsPart.state}>
|
||||
{configurationTypes.map(conf => (
|
||||
<Radio key={conf.value} id={conf.value} value={conf.value} disabled={readonly} small keepSize>
|
||||
{conf.name}
|
||||
</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</Container>
|
||||
</FormFieldDescription>
|
||||
)}
|
||||
</Container>
|
||||
{optionsPart.state.configurationType === DriverConfigurationType.Url && (
|
||||
<InputField type="text" name="url" state={optionsPart.state} readOnly={readonly || formState.isDisabled}>
|
||||
{translate('plugin_connections_connection_form_part_main_url_jdbc')}
|
||||
</InputField>
|
||||
)}
|
||||
</div>
|
||||
{optionsPart.state.configurationType === DriverConfigurationType.Url && (
|
||||
<InputField type="text" name="url" state={optionsPart.state} readOnly={readonly || formState.isDisabled} fill>
|
||||
{translate('plugin_connections_connection_form_part_main_url_jdbc')}
|
||||
</InputField>
|
||||
)}
|
||||
{optionsPart.state.configurationType === DriverConfigurationType.Manual &&
|
||||
(driver?.useCustomPage ? (
|
||||
<ObjectPropertyInfoForm
|
||||
state={optionsPart.state.mainPropertyValues}
|
||||
properties={driver.mainProperties ?? EMPTY_ARRAY}
|
||||
disabled={formState.isDisabled}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
) : (
|
||||
<ParametersForm
|
||||
config={optionsPart.state}
|
||||
embedded={driver?.embedded}
|
||||
requiresServerName={driver?.requiresServerName}
|
||||
disabled={formState.isDisabled}
|
||||
readOnly={readonly}
|
||||
originLocal={originLocal}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
|
||||
{optionsPart.state.configurationType === DriverConfigurationType.Manual &&
|
||||
(driver?.useCustomPage ? (
|
||||
<ObjectPropertyInfoForm
|
||||
state={optionsPart.state.mainPropertyValues}
|
||||
properties={driver.mainProperties ?? EMPTY_ARRAY}
|
||||
disabled={formState.isDisabled}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
) : (
|
||||
<ParametersForm
|
||||
config={optionsPart.state}
|
||||
embedded={driver?.embedded}
|
||||
requiresServerName={driver?.requiresServerName}
|
||||
disabled={formState.isDisabled}
|
||||
readOnly={readonly}
|
||||
originLocal={originLocal}
|
||||
/>
|
||||
))}
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{!driver?.anonymousAccess && (authentication.authorized || !edit) && (
|
||||
<Group form gap>
|
||||
<GroupTitle>{translate('connections_connection_edit_authentication')}</GroupTitle>
|
||||
{serverConfigResource.resource.distributed && isSharedProject && (
|
||||
<FieldCheckbox
|
||||
id={optionsPart.state.connectionId + 'isShared'}
|
||||
name="sharedCredentials"
|
||||
title={translate('connections_connection_share_credentials_tooltip')}
|
||||
state={optionsPart.state}
|
||||
disabled={formState.isDisabled || readonly}
|
||||
keepSize
|
||||
>
|
||||
{translate('connections_connection_share_credentials')}
|
||||
</FieldCheckbox>
|
||||
)}
|
||||
{authenticationVisible && (
|
||||
<section className="theme-border-color-background tw:flex tw:min-w-0 tw:flex-col tw:gap-4 tw:border-t tw:pt-6">
|
||||
<GroupTitle>{translate('connections_connection_edit_authentication')}</GroupTitle>
|
||||
{serverConfigResource.resource.distributed && isSharedProject && (
|
||||
<FieldCheckbox
|
||||
id={optionsPart.state.connectionId + 'isShared'}
|
||||
name="sharedCredentials"
|
||||
title={translate('connections_connection_share_credentials_tooltip')}
|
||||
state={optionsPart.state}
|
||||
disabled={formState.isDisabled || readonly}
|
||||
keepSize
|
||||
>
|
||||
{translate('connections_connection_share_credentials')}
|
||||
</FieldCheckbox>
|
||||
)}
|
||||
<div className="tw:grid">
|
||||
<ConnectionAuthModelSelector
|
||||
authModelCredentialsState={optionsPart.state}
|
||||
applicableAuthModels={applicableAuthModels}
|
||||
@@ -269,77 +245,49 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
|
||||
disabled={formState.isDisabled}
|
||||
onAuthModelChange={handleAuthModelSelect}
|
||||
/>
|
||||
{!sharedCredentials ? (
|
||||
<>
|
||||
{properties && (
|
||||
<ConnectionAuthModelCredentialsForm
|
||||
credentials={optionsPart.state.credentials}
|
||||
properties={properties}
|
||||
readonly={readonly}
|
||||
disabled={formState.isDisabled}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<FormFieldDescription>
|
||||
{translate('plugin_connections_connection_form_shared_credentials_manage_info')}
|
||||
<Link inline onClick={openCredentialsTab}>
|
||||
{translate('plugin_connections_connection_form_shared_credentials_manage_info_tab_link')}
|
||||
</Link>
|
||||
</FormFieldDescription>
|
||||
)}
|
||||
{!sharedCredentials && authModel && credentialsSavingEnabled && (
|
||||
<FieldCheckbox
|
||||
id={optionsPart.state.connectionId + 'authNeeded'}
|
||||
name="saveCredentials"
|
||||
state={optionsPart.state}
|
||||
disabled={formState.isDisabled || readonly || optionsPart.state.sharedCredentials}
|
||||
title={translate(
|
||||
!isSharedProject || serverConfigResource.data?.distributed
|
||||
? 'connections_connection_authentication_save_credentials_for_user_tooltip'
|
||||
: 'connections_connection_edit_save_credentials_shared_tooltip',
|
||||
)}
|
||||
keepSize
|
||||
>
|
||||
{translate(
|
||||
!isSharedProject || serverConfigResource.data?.distributed
|
||||
? 'connections_connection_authentication_save_credentials_for_user'
|
||||
: 'connections_connection_edit_save_credentials_shared',
|
||||
)}
|
||||
</FieldCheckbox>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
<ConnectionTypeForm config={optionsPart.state} />
|
||||
<Placeholder container={connectionFormService.connectionContainer} formState={formState} />
|
||||
</Container>
|
||||
<Container medium gap>
|
||||
<Placeholder container={connectionFormService.providerPropertiesContainer} formState={formState} />
|
||||
{driver?.providerProperties && <ProviderPropertiesForm formState={formState} properties={driver.providerProperties} readonly={readonly} />}
|
||||
|
||||
<AdvancedPropertiesForm config={optionsPart.state} disabled={formState.isDisabled} readonly={readonly} />
|
||||
<Group form gap>
|
||||
<Container wrap gap>
|
||||
<InputField type="text" name="name" minLength={1} state={optionsPart.state} readOnly={readonly || formState.isDisabled} required fill>
|
||||
{translate('connections_connection_name')}
|
||||
</InputField>
|
||||
<ProjectSelect
|
||||
value={formState.state.projectId}
|
||||
readOnly={readonly || edit}
|
||||
disabled={formState.isDisabled}
|
||||
autoHide
|
||||
onChange={setProject}
|
||||
/>
|
||||
<InputField type="text" name="folder" state={optionsPart.state} autoHide readOnly tiny fill>
|
||||
{translate('plugin_connections_connection_form_part_main_folder')}
|
||||
</InputField>
|
||||
</Container>
|
||||
<Textarea name="description" rows={3} state={optionsPart.state} readOnly={readonly || formState.isDisabled}>
|
||||
{translate('connections_connection_description')}
|
||||
</Textarea>
|
||||
</Group>
|
||||
</Container>
|
||||
</ColoredContainer>
|
||||
</div>
|
||||
{!sharedCredentials ? (
|
||||
<>
|
||||
{properties && (
|
||||
<ConnectionAuthModelCredentialsForm
|
||||
credentials={optionsPart.state.credentials}
|
||||
properties={properties}
|
||||
readonly={readonly}
|
||||
disabled={formState.isDisabled}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<FormFieldDescription>
|
||||
{translate('plugin_connections_connection_form_shared_credentials_manage_info')}
|
||||
<Link inline onClick={openCredentialsTab}>
|
||||
{translate('plugin_connections_connection_form_shared_credentials_manage_info_tab_link')}
|
||||
</Link>
|
||||
</FormFieldDescription>
|
||||
)}
|
||||
{!sharedCredentials && authModel && credentialsSavingEnabled && (
|
||||
<FieldCheckbox
|
||||
id={optionsPart.state.connectionId + 'authNeeded'}
|
||||
name="saveCredentials"
|
||||
state={optionsPart.state}
|
||||
disabled={formState.isDisabled || readonly || optionsPart.state.sharedCredentials}
|
||||
title={translate(
|
||||
!isSharedProject || serverConfigResource.data?.distributed
|
||||
? 'connections_connection_authentication_save_credentials_for_user_tooltip'
|
||||
: 'connections_connection_edit_save_credentials_shared_tooltip',
|
||||
)}
|
||||
keepSize
|
||||
>
|
||||
{translate(
|
||||
!isSharedProject || serverConfigResource.data?.distributed
|
||||
? 'connections_connection_authentication_save_credentials_for_user'
|
||||
: 'connections_connection_edit_save_credentials_shared',
|
||||
)}
|
||||
</FieldCheckbox>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
</ConnectionSectionWrapper>
|
||||
</Form>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { Container, InputField, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import { InputField, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import type { ConnectionConfig } from '@cloudbeaver/core-sdk';
|
||||
|
||||
interface Props {
|
||||
@@ -22,26 +22,39 @@ interface Props {
|
||||
export const ParametersForm = observer<Props>(function ParametersForm({ config, embedded, requiresServerName, disabled, readOnly, originLocal }) {
|
||||
const translate = useTranslate();
|
||||
|
||||
if (embedded) {
|
||||
return (
|
||||
<div className="tw:flex tw:flex-col tw:gap-4">
|
||||
<InputField type="text" name="databaseName" state={config} readOnly={readOnly || disabled} fill>
|
||||
{translate('plugin_connections_connection_form_part_main_custom_database')}
|
||||
</InputField>
|
||||
{requiresServerName && (
|
||||
<InputField type="text" name="serverName" state={config} readOnly={readOnly || disabled} required fill>
|
||||
{translate('plugin_connections_connection_form_part_main_custom_server_name')}
|
||||
</InputField>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container gap>
|
||||
{!embedded && (
|
||||
<Container wrap gap>
|
||||
<InputField type="text" name="host" state={config} readOnly={readOnly || !originLocal || disabled} small required>
|
||||
{translate('plugin_connections_connection_form_part_main_custom_host')}
|
||||
</InputField>
|
||||
<InputField type="number" name="port" state={config} readOnly={readOnly || !originLocal || disabled} tiny>
|
||||
{translate('plugin_connections_connection_form_part_main_custom_port')}
|
||||
</InputField>
|
||||
</Container>
|
||||
)}
|
||||
<InputField type="text" name="databaseName" state={config} readOnly={readOnly || disabled}>
|
||||
<div className="tw:flex tw:flex-col tw:gap-4">
|
||||
<div className="tw:grid tw:grid-cols-1 tw:gap-4 tw:lg:grid-cols-[minmax(0,2fr)_minmax(8rem,0.75fr)]">
|
||||
<InputField type="text" name="host" state={config} readOnly={readOnly || !originLocal || disabled} required fill>
|
||||
{translate('plugin_connections_connection_form_part_main_custom_host')}
|
||||
</InputField>
|
||||
<InputField type="number" name="port" state={config} readOnly={readOnly || !originLocal || disabled} fill>
|
||||
{translate('plugin_connections_connection_form_part_main_custom_port')}
|
||||
</InputField>
|
||||
</div>
|
||||
<InputField type="text" name="databaseName" state={config} readOnly={readOnly || disabled} fill>
|
||||
{translate('plugin_connections_connection_form_part_main_custom_database')}
|
||||
</InputField>
|
||||
{requiresServerName && (
|
||||
<InputField type="text" name="serverName" state={config} readOnly={readOnly || disabled} required>
|
||||
<InputField type="text" name="serverName" state={config} readOnly={readOnly || disabled} required fill>
|
||||
{translate('plugin_connections_connection_form_part_main_custom_server_name')}
|
||||
</InputField>
|
||||
)}
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import {
|
||||
Container,
|
||||
Expandable,
|
||||
Group,
|
||||
GroupTitle,
|
||||
ObjectPropertyInfoForm,
|
||||
useObjectPropertyCategories,
|
||||
useTranslate,
|
||||
} from '@cloudbeaver/core-blocks';
|
||||
import { Container, GroupTitle, ObjectPropertyInfoForm, useObjectPropertyCategories, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import { type DriverPropertyInfoFragment, getObjectPropertyType } from '@cloudbeaver/core-sdk';
|
||||
import type { IFormState } from '@cloudbeaver/core-ui';
|
||||
import type { IConnectionFormState } from '../IConnectionFormState.js';
|
||||
@@ -45,7 +37,7 @@ export const ProviderPropertiesForm = observer<Props>(function ProviderPropertie
|
||||
const nonBooleanProperties = supportedProperties.filter(property => !property.category && property.dataType !== 'Boolean');
|
||||
|
||||
return (
|
||||
<Group form gap>
|
||||
<section className="tw:flex tw:min-w-0 tw:flex-col tw:gap-4">
|
||||
{isUncategorizedExists && (
|
||||
<>
|
||||
<GroupTitle>{translate('ui_settings')}</GroupTitle>
|
||||
@@ -76,24 +68,23 @@ export const ProviderPropertiesForm = observer<Props>(function ProviderPropertie
|
||||
</>
|
||||
)}
|
||||
|
||||
{categories.map((category, index) => (
|
||||
{categories.map(category => (
|
||||
<Container key={`${category}_${config.driverId}`} gap>
|
||||
<Expandable label={category} defaultExpanded={index === 0}>
|
||||
<Container dense={isOnlyBooleans(supportedProperties, category)} wrap gap>
|
||||
<ObjectPropertyInfoForm
|
||||
properties={supportedProperties}
|
||||
state={config.providerProperties}
|
||||
category={category}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
getLayoutSize={property => (getObjectPropertyType(property) === 'checkbox' ? { maximum: true } : { small: true, noGrow: true })}
|
||||
hideEmptyPlaceholder
|
||||
/>
|
||||
</Container>
|
||||
</Expandable>
|
||||
<GroupTitle>{category}</GroupTitle>
|
||||
<Container dense={isOnlyBooleans(supportedProperties, category)} wrap gap>
|
||||
<ObjectPropertyInfoForm
|
||||
properties={supportedProperties}
|
||||
state={config.providerProperties}
|
||||
category={category}
|
||||
disabled={disabled}
|
||||
readOnly={readonly}
|
||||
getLayoutSize={property => (getObjectPropertyType(property) === 'checkbox' ? { maximum: true } : { small: true, noGrow: true })}
|
||||
hideEmptyPlaceholder
|
||||
/>
|
||||
</Container>
|
||||
</Container>
|
||||
))}
|
||||
</Group>
|
||||
</section>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -32,7 +32,8 @@ export class ConnectionOriginInfoTabService extends Bootstrap {
|
||||
override register(): void {
|
||||
this.connectionFormService.parts.add({
|
||||
key: 'origin',
|
||||
order: 3,
|
||||
icon: '/icons/plugin_connection_globe.svg',
|
||||
order: 5,
|
||||
tab: () => OriginInfoTab,
|
||||
panel: () => OriginInfo,
|
||||
getLoader: (context, props) => {
|
||||
|
||||
@@ -11,7 +11,6 @@ import { AUTH_PROVIDER_LOCAL_ID, AuthProvidersResource, UserInfoResource } from
|
||||
import {
|
||||
ColoredContainer,
|
||||
ExceptionMessage,
|
||||
Group,
|
||||
Loader,
|
||||
ObjectPropertyInfoForm,
|
||||
s,
|
||||
@@ -75,7 +74,7 @@ export const OriginInfo: TabContainerPanelComponent<IConnectionFormProps> = obse
|
||||
|
||||
if (connection.isLoading()) {
|
||||
return (
|
||||
<ColoredContainer className={s(style, { coloredContainer: true })}>
|
||||
<ColoredContainer className={s(style, { coloredContainer: true })} surface>
|
||||
<Loader key="static" className={s(style, { loader: true })} />
|
||||
</ColoredContainer>
|
||||
);
|
||||
@@ -83,7 +82,7 @@ export const OriginInfo: TabContainerPanelComponent<IConnectionFormProps> = obse
|
||||
|
||||
if (connection.exception) {
|
||||
return (
|
||||
<ColoredContainer className={s(style, { coloredContainer: true })}>
|
||||
<ColoredContainer className={s(style, { coloredContainer: true })} surface>
|
||||
<ExceptionMessage exception={connection.exception} onRetry={connection.reload} />
|
||||
</ColoredContainer>
|
||||
);
|
||||
@@ -91,7 +90,7 @@ export const OriginInfo: TabContainerPanelComponent<IConnectionFormProps> = obse
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<ColoredContainer className={s(style, { coloredContainer: true })} parent>
|
||||
<ColoredContainer className={s(style, { coloredContainer: true })} parent surface>
|
||||
<TextPlaceholder>
|
||||
{translate('plugin_connections_connection_cloud_auth_required', undefined, {
|
||||
providerLabel: providerLoader.data?.label,
|
||||
@@ -103,22 +102,22 @@ export const OriginInfo: TabContainerPanelComponent<IConnectionFormProps> = obse
|
||||
|
||||
if (!connectionOriginDetailsResource.data?.origin.details || connectionOriginDetailsResource.data?.origin.details.length === 0) {
|
||||
return (
|
||||
<ColoredContainer className={s(style, { coloredContainer: true })} parent>
|
||||
<ColoredContainer className={s(style, { coloredContainer: true })} parent surface>
|
||||
<TextPlaceholder>{translate('core_connections_connection_no_information')}</TextPlaceholder>
|
||||
</ColoredContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ColoredContainer className={s(style, { coloredContainer: true })} parent>
|
||||
<Group large gap>
|
||||
<ColoredContainer className={s(style, { coloredContainer: true })} parent surface>
|
||||
<div className="tw:flex tw:w-full tw:max-w-xl tw:flex-col tw:p-6">
|
||||
<ObjectPropertyInfoForm
|
||||
properties={connectionOriginDetailsResource.data.origin.details}
|
||||
readOnly
|
||||
small
|
||||
autoHide
|
||||
/>
|
||||
</Group>
|
||||
</div>
|
||||
<Loader key="overlay" className={s(style, { loader: true })} loading={connection.isLoading()} overlay />
|
||||
</ColoredContainer>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
//@TODO it's here until the profile implementation in the CloudBeaver
|
||||
export const PROFILE_AUTH_MODEL_ID = 'profile';
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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 type { IConnectionFormProps } from './IConnectionFormState.js';
|
||||
import { PROFILE_AUTH_MODEL_ID } from './PROFILE_AUTH_MODEL_ID.js';
|
||||
|
||||
export function isConnectionFormReadOnly(
|
||||
formState: Pick<IConnectionFormProps['formState'], 'isDisabled' | 'isReadOnly'>,
|
||||
authModel: string | undefined,
|
||||
): boolean {
|
||||
return formState.isDisabled || formState.isReadOnly || authModel === PROFILE_AUTH_MODEL_ID;
|
||||
}
|
||||
@@ -23,6 +23,7 @@ export * from './ConnectionForm/ConnectionFormService.js';
|
||||
export * from './ConnectionForm/CONNECTION_CONFIG_SCHEMA.js';
|
||||
export * from './ConnectionForm/Options/getConnectionFormOptionsPart.js';
|
||||
export * from './ConnectionForm/Options/ConnectionFormOptionsPart.js';
|
||||
export * from './ConnectionForm/ConnectionSectionWrapperLazy.js';
|
||||
export * from './ConnectionForm/SharedCredentials/CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID.js';
|
||||
export * from './ConnectionForm/ConnectionAuthModelCredentials/ConnectionAuthModelCredentialsForm.js';
|
||||
export * from './ContextMenu/MENU_CONNECTIONS.js';
|
||||
|
||||
@@ -3,6 +3,9 @@ export default [
|
||||
['plugin_connections_connection_form_part_main', 'Main'],
|
||||
['plugin_connections_connection_form_part_main_auth_model', 'Authentication model'],
|
||||
['plugin_connections_connection_form_part_properties', 'Driver Properties'],
|
||||
['plugin_connections_connection_form_part_advanced', 'Advanced settings'],
|
||||
['plugin_connections_connection_form_part_initialization_settings', 'Initialization settings'],
|
||||
['plugin_connections_connection_form_connection_details', 'Connection details'],
|
||||
['plugin_connections_connection_form_part_main_custom_host', 'Host'],
|
||||
['plugin_connections_connection_form_part_main_custom_port', 'Port'],
|
||||
['plugin_connections_connection_form_part_main_custom_server_name', 'Server name'],
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
export default [
|
||||
['plugin_connections_connection_form_part_main', 'Principal'],
|
||||
['plugin_connections_connection_form_part_properties', 'Propriétés du pilote'],
|
||||
['plugin_connections_connection_form_part_advanced', 'Paramètres avancés'],
|
||||
['plugin_connections_connection_form_part_initialization_settings', "Paramètres d'initialisation"],
|
||||
['plugin_connections_connection_form_connection_details', 'Détails de la connexion'],
|
||||
['plugin_connections_connection_form_part_main_auth_model', "Modèle d'authentification"],
|
||||
['plugin_connections_connection_form_part_main_custom_host', 'Hôte'],
|
||||
['plugin_connections_connection_form_part_main_custom_port', 'Port'],
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
export default [
|
||||
['plugin_connections_connection_form_part_main', 'Main'],
|
||||
['plugin_connections_connection_form_part_properties', 'Proprietà del driver'],
|
||||
['plugin_connections_connection_form_part_advanced', 'Impostazioni avanzate'],
|
||||
['plugin_connections_connection_form_part_initialization_settings', 'Impostazioni di inizializzazione'],
|
||||
['plugin_connections_connection_form_connection_details', 'Dettagli della connessione'],
|
||||
['plugin_connections_connection_form_part_main_auth_model', 'Modello di autenticazione'],
|
||||
['plugin_connections_connection_form_part_main_custom_host', 'Host'],
|
||||
['plugin_connections_connection_form_part_main_custom_port', 'Porta'],
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
export default [
|
||||
['plugin_connections_connection_form_part_main', 'Главное'],
|
||||
['plugin_connections_connection_form_part_properties', 'Параметры драйвера'],
|
||||
['plugin_connections_connection_form_part_advanced', 'Расширенные настройки'],
|
||||
['plugin_connections_connection_form_part_initialization_settings', 'Настройки инициализации'],
|
||||
['plugin_connections_connection_form_connection_details', 'Параметры подключения'],
|
||||
['plugin_connections_connection_form_part_main_auth_model', 'Модель аутентификации'],
|
||||
['plugin_connections_connection_form_part_main_custom_host', 'Хост'],
|
||||
['plugin_connections_connection_form_part_main_custom_port', 'Порт'],
|
||||
|
||||
@@ -2,6 +2,9 @@ export default [
|
||||
['plugin_connections_new_connection_dialog_title', 'Kết nối mới'],
|
||||
['plugin_connections_connection_form_part_main', 'Chính'],
|
||||
['plugin_connections_connection_form_part_properties', 'Thuộc tính Trình điều khiển (driver)'],
|
||||
['plugin_connections_connection_form_part_advanced', 'Cài đặt nâng cao'],
|
||||
['plugin_connections_connection_form_part_initialization_settings', 'Cài đặt khởi tạo'],
|
||||
['plugin_connections_connection_form_connection_details', 'Chi tiết kết nối'],
|
||||
['plugin_connections_connection_form_part_main_auth_model', 'Mô hình xác thực'],
|
||||
['plugin_connections_connection_form_part_main_custom_host', 'Máy chủ'],
|
||||
['plugin_connections_connection_form_part_main_custom_port', 'Cổng'],
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
export default [
|
||||
['plugin_connections_connection_form_part_main', '主要'],
|
||||
['plugin_connections_connection_form_part_properties', '驱动属性'],
|
||||
['plugin_connections_connection_form_part_advanced', '高级设置'],
|
||||
['plugin_connections_connection_form_part_initialization_settings', '初始化设置'],
|
||||
['plugin_connections_connection_form_connection_details', '连接详细信息'],
|
||||
['plugin_connections_connection_form_part_main_auth_model', '身份验证模型'],
|
||||
['plugin_connections_connection_form_part_main_custom_host', '主机'],
|
||||
['plugin_connections_connection_form_part_main_custom_port', '端口'],
|
||||
|
||||
@@ -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.
|
||||
@@ -20,6 +20,9 @@ import { ConnectionOptionsTabService } from './ConnectionForm/Options/Connection
|
||||
import { ConnectionDriverPropertiesTabService } from './ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService.js';
|
||||
import { ConnectionFormService } from './ConnectionForm/ConnectionFormService.js';
|
||||
import { ConnectionAuthService } from './ConnectionAuthService.js';
|
||||
import { AdvancedSettingsTabService } from './ConnectionForm/AdvancedSettings/AdvancedSettingsTabService.js';
|
||||
import { ConnectionDetailsTabService } from './ConnectionForm/ConnectionDetails/ConnectionDetailsTabService.js';
|
||||
import { InitializationSettingsTabService } from './ConnectionForm/InitializationSettings/InitializationSettingsTabService.js';
|
||||
|
||||
export default ModuleRegistry.add({
|
||||
name: '@cloudbeaver/plugin-connections',
|
||||
@@ -30,6 +33,9 @@ export default ModuleRegistry.add({
|
||||
.addSingleton(Bootstrap, proxy(ConnectionFoldersBootstrap))
|
||||
.addSingleton(Bootstrap, proxy(ConnectionMenuBootstrap))
|
||||
.addSingleton(Bootstrap, proxy(ConnectionOptionsTabService))
|
||||
.addSingleton(Bootstrap, proxy(ConnectionDetailsTabService))
|
||||
.addSingleton(Bootstrap, proxy(AdvancedSettingsTabService))
|
||||
.addSingleton(Bootstrap, proxy(InitializationSettingsTabService))
|
||||
.addSingleton(Bootstrap, proxy(ConnectionOriginInfoTabService))
|
||||
.addSingleton(Bootstrap, proxy(ConnectionsExplorerBootstrap))
|
||||
.addSingleton(Bootstrap, LocaleService)
|
||||
@@ -46,6 +52,9 @@ export default ModuleRegistry.add({
|
||||
.addSingleton(ConnectionMenuBootstrap)
|
||||
.addSingleton(ConnectionOriginInfoTabService)
|
||||
.addSingleton(ConnectionOptionsTabService)
|
||||
.addSingleton(ConnectionDetailsTabService)
|
||||
.addSingleton(AdvancedSettingsTabService)
|
||||
.addSingleton(InitializationSettingsTabService)
|
||||
.addSingleton(ConnectionDriverPropertiesTabService)
|
||||
.addSingleton(ConnectionFormService);
|
||||
},
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
Container,
|
||||
Expandable,
|
||||
FieldCheckbox,
|
||||
GroupItem,
|
||||
InputField,
|
||||
Select,
|
||||
Switch,
|
||||
@@ -166,37 +165,26 @@ export const SSHForm = observer<ISSHFormProps>(function SSHForm({
|
||||
)}
|
||||
</FieldCheckbox>
|
||||
)}
|
||||
<Container gap>
|
||||
<Expandable label={translate('plugin_network_handlers_ssh_tunnel_advanced_settings')}>
|
||||
<Container gap>
|
||||
<InputField
|
||||
type="number"
|
||||
name="aliveInterval"
|
||||
state={state.properties}
|
||||
readOnly={disabledInternal}
|
||||
labelTooltip={aliveIntervalLabel}
|
||||
tiny
|
||||
>
|
||||
{aliveIntervalLabel}
|
||||
</InputField>
|
||||
<InputField
|
||||
type="number"
|
||||
name="sshConnectTimeout"
|
||||
state={state.properties}
|
||||
readOnly={disabledInternal}
|
||||
labelTooltip={connectTimeoutLabel}
|
||||
tiny
|
||||
>
|
||||
{connectTimeoutLabel}
|
||||
</InputField>
|
||||
</Container>
|
||||
</Expandable>
|
||||
</Container>
|
||||
<GroupItem>
|
||||
<Button type="button" disabled={disabled || !enabled || testLoading || !testAvailable} loader onClick={testTunnel}>
|
||||
{translate('plugin_connection_network_handlers_ssh_test')}
|
||||
</Button>
|
||||
</GroupItem>
|
||||
<Expandable label={translate('plugin_network_handlers_ssh_tunnel_advanced_settings')}>
|
||||
<Container gap>
|
||||
<InputField type="number" name="aliveInterval" state={state.properties} readOnly={disabledInternal} labelTooltip={aliveIntervalLabel} tiny>
|
||||
{aliveIntervalLabel}
|
||||
</InputField>
|
||||
<InputField
|
||||
type="number"
|
||||
name="sshConnectTimeout"
|
||||
state={state.properties}
|
||||
readOnly={disabledInternal}
|
||||
labelTooltip={connectTimeoutLabel}
|
||||
tiny
|
||||
>
|
||||
{connectTimeoutLabel}
|
||||
</InputField>
|
||||
</Container>
|
||||
</Expandable>
|
||||
<Button type="button" disabled={disabled || !enabled || testLoading || !testAvailable} loader onClick={testTunnel}>
|
||||
{translate('plugin_connection_network_handlers_ssh_test')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
import { SSH_TUNNEL_ID } from '../NetworkHandlerResource.js';
|
||||
import { NetworkHandlerAuthType, type NetworkHandlerConfigInput } from '@cloudbeaver/core-sdk';
|
||||
import { trimObjectValues } from '@cloudbeaver/core-utils';
|
||||
import { toJS } from 'mobx';
|
||||
|
||||
export const SSH_DEFAULT_HANDLER_CONFIG: () => NetworkHandlerConfigInput = () => ({
|
||||
@@ -60,24 +61,17 @@ export function getSSHHandlerConfig(
|
||||
|
||||
delete handlerConfig.secureProperties;
|
||||
|
||||
return trimSSHConfig(handlerConfig);
|
||||
return handlerConfig;
|
||||
}
|
||||
|
||||
function trimSSHConfig(input: NetworkHandlerConfigInput): NetworkHandlerConfigInput {
|
||||
export function trimSSHConfig(input: NetworkHandlerConfigInput): NetworkHandlerConfigInput {
|
||||
const trimmedInput = toJS(input);
|
||||
const attributesToTrim = Object.keys(input) as (keyof NetworkHandlerConfigInput)[];
|
||||
|
||||
for (const key of attributesToTrim) {
|
||||
if (typeof trimmedInput[key] === 'string') {
|
||||
trimmedInput[key] = trimmedInput[key]?.trim();
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in (trimmedInput.properties ?? {})) {
|
||||
if (typeof trimmedInput.properties[key] === 'string') {
|
||||
trimmedInput.properties[key] = trimmedInput.properties[key]?.trim();
|
||||
}
|
||||
}
|
||||
trimObjectValues(trimmedInput);
|
||||
trimObjectValues(trimmedInput.properties);
|
||||
|
||||
return trimmedInput;
|
||||
}
|
||||
|
||||
export function trimSSHConfigInConfigurations(configurations: NetworkHandlerConfigInput[]): NetworkHandlerConfigInput[] {
|
||||
return configurations.map(config => (config.id === SSH_TUNNEL_ID ? trimSSHConfig(config) : config));
|
||||
}
|
||||
|
||||