From 3f2c4f94e4f360056d977d0e98757617dfa13534 Mon Sep 17 00:00:00 2001 From: Ainur Date: Wed, 2 Sep 2026 09:39:16 +0200 Subject: [PATCH 01/31] dbeaver/pro#9532 Support user AI credentials --- .../model/app/VoidSecretController.java | 9 +- .../model/session/WebUserContext.java | 4 +- .../schema/service.ai.graphqls | 15 +- .../service/ai/WebAIProfileCredentials.java | 359 ++++++++++++++++++ .../io/cloudbeaver/service/ai/WebAIUtils.java | 26 +- .../service/ai/gql/DBWServiceAI.java | 8 + .../service/ai/gql/WebServiceAI.java | 38 +- .../service/ai/gql/WebServiceBindingAI.java | 7 + .../ai/model/WebAIConfigurationProfile.java | 9 + .../inputs/WebAIProfileCredentialsInput.java | 24 ++ .../META-INF/MANIFEST.MF | 1 + .../ai/WebAIProfileCredentialsTest.java | 145 +++++++ .../test/platform/CEServerTestSuite.java | 2 + 13 files changed, 637 insertions(+), 10 deletions(-) create mode 100644 server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIProfileCredentials.java create mode 100644 server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/model/inputs/WebAIProfileCredentialsInput.java create mode 100644 server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/service/ai/WebAIProfileCredentialsTest.java diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/VoidSecretController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/VoidSecretController.java index f3fa1706391..eb0b9f787c2 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/VoidSecretController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/VoidSecretController.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2024 DBeaver Corp and others + * 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. @@ -40,6 +40,11 @@ public VoidSecretController() { } + @Override + public long getSupportedFeatures() { + return 0; + } + @Nullable @Override public String getPrivateSecretValue(@NotNull String secretId) { @@ -68,4 +73,4 @@ public void authorize( ) throws DBException { } -} \ No newline at end of file +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java index 5e96668bbbb..381483fdc85 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebUserContext.java @@ -238,8 +238,8 @@ private void setUserPermissions(Set permissions) { } @NotNull - public DBSSecretController getSecretController() throws DBException { - if (this.securityController == null) { + public synchronized DBSSecretController getSecretController() throws DBException { + if (this.secretController == null) { this.secretController = application.getSecretController(this, workspace.getAuthContext()); } return secretController; diff --git a/server/bundles/io.cloudbeaver.service.ai/schema/service.ai.graphqls b/server/bundles/io.cloudbeaver.service.ai/schema/service.ai.graphqls index 293c4fbfedb..5d8a6fbed6f 100644 --- a/server/bundles/io.cloudbeaver.service.ai/schema/service.ai.graphqls +++ b/server/bundles/io.cloudbeaver.service.ai/schema/service.ai.graphqls @@ -120,6 +120,10 @@ type AIConfigurationProfileInfo @since(version: "26.1.3") { name: String! "ID of the AI engine that this profile belongs to." engineId: ID! + "Whether this profile uses global administrator-managed credentials." + global: Boolean! @since(version: "26.2.0") + "Whether the current user has saved credentials for this profile." + credentialsSaved: Boolean! @since(version: "26.2.0") } type AIAdminConfigurationProfileInfo @since(version: "26.1.3") { @@ -129,6 +133,8 @@ type AIAdminConfigurationProfileInfo @since(version: "26.1.3") { name: String! "ID of the AI engine that this profile belongs to." engineId: ID! + "Whether this profile uses global administrator-managed credentials." + global: Boolean! @since(version: "26.2.0") "Configuration of the AI engine profile." configuration: [ObjectPropertyInfo!]! } @@ -226,13 +232,18 @@ input AIConfigurationProfileInput @since(version: "26.1.3") { configuration: AIEngineConfig } +input AIProfileCredentialsInput @since(version: "26.2.0") { + "Credential property values keyed by AI engine property ID." + properties: Object! +} + extend type Query @since(version: "23.2.2") { "Returns the list of available AI engines." aiListEngines: [AIEngineInfo!] "Returns the global AI settings." aiSettings: AISettingsInfo! "Returns the properties of the specified AI engine for displaying it in the UI. In case settings are passed, fills it with defaults and returns back." - aiListEngineProperties(engineId: ID!, profileId: ID, settings : AIEngineConfig): [ObjectPropertyInfo!]! + aiListEngineProperties(engineId: ID!, profileId: ID, settings: AIEngineConfig): [ObjectPropertyInfo!]! "Returns models available for the specified AI engine configuration." aiListEngineModels(engineId: ID!, profileId: ID, settings: AIEngineConfig): [AIModelInfo!]! @since(version: "26.2.0") @@ -267,6 +278,8 @@ extend type Mutation @since(version: "23.2.2") { aiUpdateProfile(config: AIConfigurationProfileInput!): AIAdminConfigurationProfileInfo! @since(version: "26.1.3") "Deletes the specified AI engine configuration profile." aiDeleteProfile(profileId: ID!): Boolean! @since(version: "26.1.3") + "Saves or updates the current user's credentials for a non-global profile. Empty values clear credentials." + aiSaveProfileCredentials(profileId: ID!, credentials: AIProfileCredentialsInput!): Boolean! @since(version: "26.2.0") """ Creates a new AI chat conversation. diff --git a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIProfileCredentials.java b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIProfileCredentials.java new file mode 100644 index 00000000000..16d597ca830 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIProfileCredentials.java @@ -0,0 +1,359 @@ +/* + * 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.service.ai; + +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.model.session.WebSession; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.ai.AIConfigurationProfile; +import org.jkiss.dbeaver.model.ai.engine.AIEngineProperties; +import org.jkiss.dbeaver.model.ai.registry.AISettingsManager; +import org.jkiss.dbeaver.model.auth.AuthProperty; +import org.jkiss.dbeaver.model.secret.DBSSecretController; +import org.jkiss.dbeaver.model.secret.DBSSecretObject; +import org.jkiss.dbeaver.model.secret.DBSSecretValue; +import org.jkiss.dbeaver.runtime.properties.ObjectAttributeDescriptor; +import org.jkiss.dbeaver.runtime.properties.ObjectPropertyDescriptor; +import org.jkiss.dbeaver.runtime.properties.PropertySourceEditable; +import org.jkiss.utils.CommonUtils; + +import java.util.*; + +public final class WebAIProfileCredentials { + private static final String SECRET_ID_PREFIX = "ai.profile."; + private static final String SECRET_OBJECT_TYPE = "aiProfile"; + private static final String SESSION_CREDENTIALS_ATTRIBUTE_PREFIX = "ai.profile.credentials."; + + private WebAIProfileCredentials() { + } + + public static boolean areCredentialsSaved( + @NotNull WebSession webSession, + @NotNull AIConfigurationProfile profile + ) throws DBException { + if (profile.isGlobal() || webSession.getUserId() == null || !webSession.isAuthorizedInSecurityManager()) { + return false; + } + DBSSecretController secretController = webSession.getUserContext().getSecretController(); + Set credentialProperties = getCredentialPropertyIds(profile.getConfiguration()); + Map storedCredentials = isPersistentStorageAvailable(secretController) + ? getStoredCredentials(secretController, profile, credentialProperties) + : getSessionCredentials(webSession, profile, false); + return !storedCredentials.isEmpty(); + } + + public static void saveCredentials( + @NotNull WebSession webSession, + @NotNull AIConfigurationProfile profile, + @NotNull Map credentials + ) throws DBException { + validateUserProfile(webSession, profile); + DBSSecretController secretController = webSession.getUserContext().getSecretController(); + Set credentialProperties = getCredentialPropertyIds(profile.getConfiguration()); + if (!isPersistentStorageAvailable(secretController)) { + Map sessionCredentials = getSessionCredentials(webSession, profile, true); + updateCredentials(sessionCredentials, credentialProperties, credentials); + return; + } + validateCredentialProperties(credentialProperties, credentials.keySet()); + for (Map.Entry credential : credentials.entrySet()) { + String value = credential.getValue() == null ? null : credential.getValue().toString(); + String secretId = getSecretId(profile, credential.getKey()); + if (CommonUtils.isEmpty(value)) { + secretController.setPrivateSecretValue(secretId, null); + } else { + secretController.setPrivateSecretValue( + getSecretObject(profile), + new DBSSecretValue(secretId, profile.getProfileName() + ": " + credential.getKey(), value) + ); + } + } + } + + @NotNull + public static AIConfigurationProfile getEffectiveProfile( + @NotNull WebSession webSession, + @NotNull AIConfigurationProfile profile + ) throws DBException { + AIConfigurationProfile source = AISettingsManager.getStaticSettings() + .getConfigurationOrNull(profile.getProfileId()); + if (source == null) { + throw new DBWebException("AI profile does not exist"); + } + if (source.isGlobal()) { + return source; + } + + validateUserProfile(webSession, source); + DBSSecretController secretController = webSession.getUserContext().getSecretController(); + Map credentials = isPersistentStorageAvailable(secretController) + ? getStoredCredentials(secretController, source, getCredentialPropertyIds(source.getConfiguration())) + : getSessionCredentials(webSession, source, false); + if (credentials.isEmpty()) { + throw new DBWebException("AI profile credentials are not configured"); + } + + AIEngineProperties sourceProperties = source.getConfiguration(); + AIEngineProperties effectiveProperties = AISettingsManager.READ_PROPS_GSON.fromJson( + AISettingsManager.READ_PROPS_GSON.toJson(sourceProperties), + sourceProperties.getClass() + ); + applyCredentials(webSession, effectiveProperties, credentials); + + AIConfigurationProfile effectiveProfile = new AIConfigurationProfile(); + effectiveProfile.setProfileId(source.getProfileId()); + effectiveProfile.setProfileName(source.getProfileName()); + effectiveProfile.setEngineId(source.getEngineId()); + effectiveProfile.setConfiguration(effectiveProperties); + effectiveProfile.setGlobal(false); + return effectiveProfile; + } + + public static void prepareGlobalProfile( + @NotNull WebSession webSession, + @NotNull AIConfigurationProfile profile + ) throws DBException { + if (profile.isGlobal()) { + return; + } + AIEngineProperties properties = profile.getConfiguration(); + Map emptyCredentials = new HashMap<>(); + getCredentialPropertyIds(properties).forEach(property -> emptyCredentials.put(property, null)); + applyCredentials(webSession, properties, emptyCredentials); + } + + public static void validateCredentialsSupport( + @NotNull WebSession webSession, + @NotNull AIEngineProperties properties + ) throws DBException { + if (getCredentialPropertyIds(properties).isEmpty()) { + throw new DBWebException("AI engine does not support user credentials"); + } + } + + public static void deleteCredentials( + @NotNull WebSession webSession, + @NotNull AIConfigurationProfile profile + ) throws DBException { + DBSSecretController secretController = webSession.getUserContext().getSecretController(); + if (isPersistentStorageAvailable(secretController)) { + secretController.deleteObjectSecrets(getSecretObject(profile)); + } else { + webSession.removeAttribute(getSessionCredentialsAttribute(profile)); + } + } + + private static void validateUserProfile( + @NotNull WebSession webSession, + @NotNull AIConfigurationProfile profile + ) throws DBException { + if (webSession.getUserId() == null || !webSession.isAuthorizedInSecurityManager()) { + throw new DBWebException("User authentication is required"); + } + if (profile.isGlobal()) { + throw new DBWebException("AI profile does not use user credentials"); + } + if (getCredentialPropertyIds(profile.getConfiguration()).isEmpty()) { + throw new DBWebException("AI engine does not support user credentials"); + } + } + + private static void updateCredentials( + @NotNull Map target, + @NotNull Set credentialProperties, + @NotNull Map updates + ) throws DBWebException { + validateCredentialProperties(credentialProperties, updates.keySet()); + synchronized (target) { + for (Map.Entry credential : updates.entrySet()) { + String value = credential.getValue() == null ? null : credential.getValue().toString(); + if (CommonUtils.isEmpty(value)) { + target.remove(credential.getKey()); + } else { + target.put(credential.getKey(), value); + } + } + } + } + + private static void validateCredentialProperties( + @NotNull Set credentialProperties, + @NotNull Set updates + ) throws DBWebException { + for (String property : updates) { + if (!credentialProperties.contains(property)) { + throw new DBWebException("Invalid AI credential property " + property); + } + } + } + + private static boolean isPersistentStorageAvailable(@NotNull DBSSecretController secretController) throws DBException { + long features = secretController.getSupportedFeatures(); + return (features & DBSSecretController.FEATURE_PRIVATE_SECRETS_VIEW) != 0 && + (features & DBSSecretController.FEATURE_PRIVATE_SECRETS_EDIT) != 0; + } + + @NotNull + private static Map getSessionCredentials( + @NotNull WebSession webSession, + @NotNull AIConfigurationProfile profile, + boolean create + ) { + String attribute = getSessionCredentialsAttribute(profile); + synchronized (webSession) { + SessionCredentials sessionCredentials = webSession.getAttribute(attribute); + if (sessionCredentials != null && sessionCredentials.profile() == profile) { + if (create) { + return sessionCredentials.credentials(); + } + synchronized (sessionCredentials.credentials()) { + return Map.copyOf(sessionCredentials.credentials()); + } + } + if (!create) { + return Map.of(); + } + SessionCredentials newCredentials = new SessionCredentials(profile, new HashMap<>()); + webSession.setAttribute(attribute, newCredentials); + return newCredentials.credentials(); + } + } + + @NotNull + private static String getSessionCredentialsAttribute(@NotNull AIConfigurationProfile profile) { + return SESSION_CREDENTIALS_ATTRIBUTE_PREFIX + profile.getProfileId(); + } + + private static void applyCredentials( + @NotNull WebSession webSession, + @NotNull AIEngineProperties properties, + @NotNull Map credentials + ) throws DBException { + PropertySourceEditable propertySource = createPropertySource(properties); + for (Map.Entry credential : credentials.entrySet()) { + if (propertySource.getProperty(credential.getKey()) == null) { + throw new DBWebException("AI engine credential property is not available: " + credential.getKey()); + } + propertySource.setPropertyValue( + webSession.getProgressMonitor(), + credential.getKey(), + credential.getValue() + ); + } + } + + @NotNull + private static Set getCredentialPropertyIds(@NotNull AIEngineProperties properties) { + Set credentialProperties = new LinkedHashSet<>(); + for (ObjectPropertyDescriptor property : ObjectAttributeDescriptor.extractAnnotations( + null, + properties.getClass(), + null, + null, + false + )) { + if (property.isPassword() || property.getAnnotation(AuthProperty.class) != null) { + credentialProperties.add(property.getId()); + } + } + return credentialProperties; + } + + @NotNull + private static PropertySourceEditable createPropertySource(@NotNull AIEngineProperties properties) { + PropertySourceEditable propertySource = new PropertySourceEditable(properties, properties); + for (ObjectPropertyDescriptor property : ObjectAttributeDescriptor.extractAnnotations( + propertySource, + properties.getClass(), + null, + null, + false + )) { + propertySource.addProperty(property); + } + return propertySource; + } + + @NotNull + private static Map getStoredCredentials( + @NotNull DBSSecretController secretController, + @NotNull AIConfigurationProfile profile, + @NotNull Set credentialProperties + ) throws DBException { + Map credentials = new HashMap<>(); + for (String property : credentialProperties) { + String value = secretController.getPrivateSecretValue(getSecretId(profile, property)); + if (CommonUtils.isNotEmpty(value)) { + credentials.put(property, value); + } + } + return credentials; + } + + @NotNull + private static String getSecretId(@NotNull AIConfigurationProfile profile, @NotNull String propertyId) { + return getSecretIdPrefix(profile) + propertyId; + } + + @NotNull + private static String getSecretIdPrefix(@NotNull AIConfigurationProfile profile) { + return SECRET_ID_PREFIX + profile.getProfileId() + "."; + } + + @NotNull + private static DBSSecretObject getSecretObject(@NotNull AIConfigurationProfile profile) { + return new AIProfileSecretObject(profile.getProfileId()); + } + + private static final class AIProfileSecretObject implements DBSSecretObject { + @NotNull + private final String projectId = ""; + @NotNull + private final String secretObjectId; + @NotNull + private final String secretObjectType = SECRET_OBJECT_TYPE; + + private AIProfileSecretObject(@NotNull String secretObjectId) { + this.secretObjectId = secretObjectId; + } + + @NotNull + @Override + public String getProjectId() { + return projectId; + } + + @NotNull + @Override + public String getSecretObjectId() { + return secretObjectId; + } + + @NotNull + @Override + public String getSecretObjectType() { + return secretObjectType; + } + } + + private record SessionCredentials( + @NotNull AIConfigurationProfile profile, + @NotNull Map credentials + ) { + } +} diff --git a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIUtils.java b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIUtils.java index cff82e72173..7c4c103a9dc 100644 --- a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIUtils.java +++ b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIUtils.java @@ -36,7 +36,7 @@ import org.jkiss.dbeaver.model.ai.qm.AIChatStorage; import org.jkiss.dbeaver.model.ai.quota.UserTokenQuotaService; import org.jkiss.dbeaver.model.ai.registry.AIAssistantRegistry; -import org.jkiss.dbeaver.model.ai.utils.AIUtils; +import org.jkiss.dbeaver.model.ai.registry.AISettingsManager; import org.jkiss.dbeaver.model.app.DBPProject; import org.jkiss.dbeaver.model.navigator.DBNDatabaseNode; import org.jkiss.dbeaver.model.navigator.DBNNode; @@ -147,6 +147,18 @@ public static CompletableFuture scheduleConversationSubmissi @Override protected IStatus run(@NotNull DBRProgressMonitor monitor) { try { + AIConfigurationProfile selectedProfile = conversation.getProfile(); + if (selectedProfile == null) { + selectedProfile = AISettingsManager.getStaticSettings().getDefaultConfiguration(); + } + AIConfigurationProfile effectiveProfile = WebAIProfileCredentials.getEffectiveProfile( + webSession, + selectedProfile + ); + if (!effectiveProfile.getConfiguration().isValidConfiguration()) { + throw new DBWebException("Invalid AI configuration"); + } + conversation.setProfile(effectiveProfile); AIChatResponseConsumer subscriber = new WebAiChatResponseConsumer(conversation, webSession, aiChatSession); aiChatSession.processAICompletion( monitor, @@ -270,6 +282,15 @@ public static WebAISendChatMessageInfo submitPrompt( throw new DBWebException("AI services restricted for '%s'. Please contact your administrator if you need it.".formatted( conversation.getDataSource())); } + AIConfigurationProfile selectedProfile = conversation.getProfile(); + if (selectedProfile == null) { + selectedProfile = AISettingsManager.getStaticSettings().getDefaultConfiguration(); + } + AIConfigurationProfile effectiveProfile = WebAIProfileCredentials.getEffectiveProfile(webSession, selectedProfile); + if (!effectiveProfile.getConfiguration().isValidConfiguration()) { + throw new DBWebException("Invalid AI configuration"); + } + conversation.setProfile(effectiveProfile); AIChatMessage promptMessage; AIChatMessage result; synchronized (conversation) { @@ -280,9 +301,6 @@ public static WebAISendChatMessageInfo submitPrompt( if (!CommonUtils.equalObjects(caption, conversation.getCaption())) { aiChatSession.notifyConversationRenamed(conversation, conversation.getCaption()); } - if (!AIUtils.hasValidConfiguration()) { - throw new DBWebException("Invalid AI configuration"); - } if (webSession.getAttribute(WebAIUtils.getWaitingAttr(conversation)) != null) { throw new DBWebException("Conversation is already waiting for response"); } diff --git a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/gql/DBWServiceAI.java b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/gql/DBWServiceAI.java index f20f592cfe2..c7ddfbb6077 100644 --- a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/gql/DBWServiceAI.java +++ b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/gql/DBWServiceAI.java @@ -25,6 +25,7 @@ import io.cloudbeaver.service.ai.model.inputs.DataSourceId; import io.cloudbeaver.service.ai.model.inputs.WebAIChatConversationInput; import io.cloudbeaver.service.ai.model.inputs.WebAIConfigurationProfileInput; +import io.cloudbeaver.service.ai.model.inputs.WebAIProfileCredentialsInput; import io.cloudbeaver.service.ai.model.inputs.WebAiChatCompletionSettingsInput; import io.cloudbeaver.service.sql.WebSQLContextInfo; import jakarta.servlet.http.HttpServletRequest; @@ -188,4 +189,11 @@ WebAIConfigurationProfile updateProfile( @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) boolean deleteProfile(@NotNull WebSession webSession, @NotNull String profileId) throws DBWebException; + + @WebAction + boolean saveProfileCredentials( + @NotNull WebSession webSession, + @NotNull String profileId, + @WebParameterSecure @NotNull WebAIProfileCredentialsInput credentials + ) throws DBWebException; } diff --git a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/gql/WebServiceAI.java b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/gql/WebServiceAI.java index f0b81fe5b7e..6429ff37e2d 100644 --- a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/gql/WebServiceAI.java +++ b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/gql/WebServiceAI.java @@ -23,12 +23,14 @@ import io.cloudbeaver.model.session.WebAsyncTaskProcessor; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.service.ai.WebAIProfileCredentials; import io.cloudbeaver.service.ai.WebAIUtils; import io.cloudbeaver.service.ai.model.*; import io.cloudbeaver.service.ai.model.events.WSAiChatMessageEvent; import io.cloudbeaver.service.ai.model.inputs.DataSourceId; import io.cloudbeaver.service.ai.model.inputs.WebAIChatConversationInput; import io.cloudbeaver.service.ai.model.inputs.WebAIConfigurationProfileInput; +import io.cloudbeaver.service.ai.model.inputs.WebAIProfileCredentialsInput; import io.cloudbeaver.service.ai.model.inputs.WebAiChatCompletionSettingsInput; import io.cloudbeaver.service.sql.WebSQLContextInfo; import io.cloudbeaver.service.sql.WebSQLProcessor; @@ -238,6 +240,7 @@ public boolean saveEngineConfiguration( AISettings settings = AISettingsManager.getInstance().getSettings(); AIConfigurationProfile profile = settings.getConfiguration(profileId); profile.setConfiguration(toEngineConfiguration(webSession.getProgressMonitor(), profile, engineSettingsInput)); + WebAIProfileCredentials.prepareGlobalProfile(webSession, profile); AISettingsManager.getInstance().saveSettings(); addAISettingsChangedEvent(webSession); return true; @@ -271,12 +274,16 @@ public void run(DBRProgressMonitor monitor) throws InvocationTargetException { .build(); AIAssistant assistant = AIAssistantRegistry.getInstance().getAssistant(webSession.getWorkspace()); + AIConfigurationProfile profile = WebAIProfileCredentials.getEffectiveProfile( + webSession, + AISettingsManager.getStaticSettings().getDefaultConfiguration() + ); AIFunctionContext fc = new AIFunctionContext(monitor, dbContext, new AIPromptGenerateSql()); AIMessage userMessage = AIMessage.userMessage(request); AIAssistantResponse result = assistant.generateText( monitor, - AISettingsManager.getStaticSettings().getDefaultConfiguration(), + profile, fc, List.of(userMessage) ); @@ -547,6 +554,10 @@ public WebAIConfigurationProfile createProfile( if (input.configuration() != null) { profile.setConfiguration(toEngineConfiguration(webSession.getProgressMonitor(), profile, input.configuration())); } + if (!profile.isGlobal()) { + WebAIProfileCredentials.validateCredentialsSupport(webSession, profile.getConfiguration()); + } + WebAIProfileCredentials.prepareGlobalProfile(webSession, profile); AISettingsManager.getInstance().saveSettings(); addAISettingsChangedEvent(webSession); return new WebAIConfigurationProfile(webSession, settings.getConfiguration(input.profileId())); @@ -566,12 +577,20 @@ public WebAIConfigurationProfile updateProfile( try { AISettings settings = AISettingsManager.getInstance().getSettings(); AIConfigurationProfile profile = settings.getConfiguration(input.profileId()); + boolean wasGlobal = profile.isGlobal(); if (input.profileName() != null) { profile.setProfileName(input.profileName()); } if (input.configuration() != null) { profile.setConfiguration(toEngineConfiguration(webSession.getProgressMonitor(), profile, input.configuration())); } + if (!wasGlobal && profile.isGlobal()) { + WebAIProfileCredentials.deleteCredentials(webSession, profile); + } + if (!profile.isGlobal()) { + WebAIProfileCredentials.validateCredentialsSupport(webSession, profile.getConfiguration()); + } + WebAIProfileCredentials.prepareGlobalProfile(webSession, profile); AISettingsManager.getInstance().saveSettings(); addAISettingsChangedEvent(webSession); return new WebAIConfigurationProfile(webSession, profile); @@ -586,6 +605,7 @@ public boolean deleteProfile(@NotNull WebSession webSession, @NotNull String pro try { AISettings settings = AISettingsManager.getInstance().getSettings(); AIConfigurationProfile profile = settings.getConfiguration(profileId); + WebAIProfileCredentials.deleteCredentials(webSession, profile); settings.removeConfiguration(profile); AISettingsManager.getInstance().saveSettings(); addAISettingsChangedEvent(webSession); @@ -595,6 +615,22 @@ public boolean deleteProfile(@NotNull WebSession webSession, @NotNull String pro return true; } + @Override + public boolean saveProfileCredentials( + @NotNull WebSession webSession, + @NotNull String profileId, + @NotNull WebAIProfileCredentialsInput credentials + ) throws DBWebException { + WebAIUtils.validateAiPluginEnabled(); + try { + AIConfigurationProfile profile = AISettingsManager.getInstance().getSettings().getConfiguration(profileId); + WebAIProfileCredentials.saveCredentials(webSession, profile, credentials.properties()); + return true; + } catch (DBException e) { + throw new DBWebException("Error saving credentials for AI profile " + profileId, e); + } + } + @NotNullWhen("dataSourceId != null") private DBPDataSourceContainer getDataSource( @NotNull WebSession webSession, diff --git a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/gql/WebServiceBindingAI.java b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/gql/WebServiceBindingAI.java index d43d958e74e..c3e53271ee1 100644 --- a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/gql/WebServiceBindingAI.java +++ b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/gql/WebServiceBindingAI.java @@ -25,6 +25,7 @@ import io.cloudbeaver.service.ai.model.inputs.DataSourceId; import io.cloudbeaver.service.ai.model.inputs.WebAIChatConversationInput; import io.cloudbeaver.service.ai.model.inputs.WebAIConfigurationProfileInput; +import io.cloudbeaver.service.ai.model.inputs.WebAIProfileCredentialsInput; import io.cloudbeaver.service.ai.model.inputs.WebAiChatCompletionSettingsInput; import io.cloudbeaver.service.sql.WebServiceBindingSQL; import org.jkiss.code.NotNull; @@ -192,6 +193,12 @@ public void bindWiring(DBWBindingContext model) { getWebSession(env), getArgumentVal(env, "profileId") ) + ).dataFetcher( + "aiSaveProfileCredentials", env -> getService(env).saveProfileCredentials( + getWebSession(env), + getArgumentVal(env, "profileId"), + JSONUtils.deserializeObject(getArgumentVal(env, "credentials"), WebAIProfileCredentialsInput.class) + ) ); } diff --git a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/model/WebAIConfigurationProfile.java b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/model/WebAIConfigurationProfile.java index a811b32ad42..d29684cd77f 100644 --- a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/model/WebAIConfigurationProfile.java +++ b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/model/WebAIConfigurationProfile.java @@ -19,6 +19,7 @@ import io.cloudbeaver.WebServiceUtils; import io.cloudbeaver.model.WebPropertyInfo; import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.service.ai.WebAIProfileCredentials; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.ai.AIConfigurationProfile; @@ -50,6 +51,14 @@ public String getEngineId() { return profile.getEngineId(); } + public boolean isGlobal() { + return profile.isGlobal(); + } + + public boolean isCredentialsSaved() throws DBException { + return WebAIProfileCredentials.areCredentialsSaved(webSession, profile); + } + @NotNull public WebPropertyInfo[] getConfiguration() throws DBException { return WebServiceUtils.getObjectFilteredProperties(webSession, profile.getConfiguration(), null); diff --git a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/model/inputs/WebAIProfileCredentialsInput.java b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/model/inputs/WebAIProfileCredentialsInput.java new file mode 100644 index 00000000000..e6997695512 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/model/inputs/WebAIProfileCredentialsInput.java @@ -0,0 +1,24 @@ +/* + * 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.service.ai.model.inputs; + +import org.jkiss.code.NotNull; + +import java.util.Map; + +public record WebAIProfileCredentialsInput(@NotNull Map properties) { +} diff --git a/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF b/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF index d4d9993dbef..44041dabefe 100644 --- a/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF +++ b/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF @@ -23,6 +23,7 @@ Require-Bundle: org.eclipse.core.runtime, io.cloudbeaver.server.ce, io.cloudbeaver.resources.drivers.base, io.cloudbeaver.product.ce, + io.cloudbeaver.service.ai, io.cloudbeaver.service.auth, io.cloudbeaver.service.rm, io.cloudbeaver.service.rm.nio, diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/service/ai/WebAIProfileCredentialsTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/service/ai/WebAIProfileCredentialsTest.java new file mode 100644 index 00000000000..e2b1e0c5425 --- /dev/null +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/service/ai/WebAIProfileCredentialsTest.java @@ -0,0 +1,145 @@ +/* + * 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.service.ai; + +import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.model.session.WebUserContext; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.ai.AIConfigurationProfile; +import org.jkiss.dbeaver.model.ai.engine.openai.OpenAIProperties; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.secret.DBSSecretController; +import org.jkiss.dbeaver.model.secret.DBSSecretObject; +import org.jkiss.dbeaver.model.secret.DBSSecretValue; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.HashMap; +import java.util.Map; + +public class WebAIProfileCredentialsTest { + private final Map secrets = new HashMap<>(); + private final Map sessionAttributes = new HashMap<>(); + private DBSSecretController secretController; + private WebSession webSession; + private AIConfigurationProfile profile; + private OpenAIProperties properties; + private String credentialPropertyId; + + @BeforeEach + public void setUp() throws DBException { + secretController = Mockito.mock(DBSSecretController.class); + Mockito.when(secretController.getSupportedFeatures()).thenReturn( + DBSSecretController.FEATURE_PRIVATE_SECRETS_VIEW | DBSSecretController.FEATURE_PRIVATE_SECRETS_EDIT + ); + Mockito.when(secretController.getPrivateSecretValue(Mockito.anyString())) + .thenAnswer(invocation -> secrets.get(invocation.getArgument(0, String.class))); + Mockito.doAnswer(invocation -> { + String id = invocation.getArgument(0, String.class); + String value = invocation.getArgument(1, String.class); + if (value == null) { + secrets.remove(id); + } else { + secrets.put(id, value); + } + return null; + }).when(secretController).setPrivateSecretValue(Mockito.anyString(), Mockito.nullable(String.class)); + Mockito.doAnswer(invocation -> { + DBSSecretValue secret = invocation.getArgument(1, DBSSecretValue.class); + secrets.put(secret.getId(), secret.getValue()); + return null; + }).when(secretController).setPrivateSecretValue( + Mockito.any(DBSSecretObject.class), + Mockito.any(DBSSecretValue.class) + ); + + WebUserContext userContext = Mockito.mock(WebUserContext.class); + Mockito.when(userContext.getSecretController()).thenReturn(secretController); + + webSession = Mockito.mock(WebSession.class); + Mockito.when(webSession.getUserId()).thenReturn("test-user"); + Mockito.when(webSession.isAuthorizedInSecurityManager()).thenReturn(true); + Mockito.when(webSession.getUserContext()).thenReturn(userContext); + Mockito.when(webSession.getProgressMonitor()).thenReturn(Mockito.mock(DBRProgressMonitor.class)); + Mockito.when(webSession.getAttribute(Mockito.anyString())) + .thenAnswer(invocation -> sessionAttributes.get(invocation.getArgument(0, String.class))); + Mockito.doAnswer(invocation -> { + sessionAttributes.put(invocation.getArgument(0, String.class), invocation.getArgument(1)); + return null; + }).when(webSession).setAttribute(Mockito.anyString(), Mockito.any()); + Mockito.doAnswer(invocation -> { + sessionAttributes.remove(invocation.getArgument(0, String.class)); + return null; + }).when(webSession).removeAttribute(Mockito.anyString()); + + properties = new OpenAIProperties(); + properties.setGlobal(false); + credentialPropertyId = "token"; + + profile = Mockito.mock(AIConfigurationProfile.class); + Mockito.when(profile.getProfileId()).thenReturn("test-profile"); + Mockito.when(profile.getProfileName()).thenReturn("Test profile"); + Mockito.when(profile.getEngineId()).thenReturn("openai"); + Mockito.when(profile.getConfiguration()).thenReturn(properties); + Mockito.when(profile.isGlobal()).thenReturn(false); + } + + @Test + public void savesUpdatesAndClearsCredentials() throws DBException { + WebAIProfileCredentials.saveCredentials(webSession, profile, Map.of(credentialPropertyId, "first")); + Assertions.assertTrue(WebAIProfileCredentials.areCredentialsSaved(webSession, profile)); + + WebAIProfileCredentials.saveCredentials(webSession, profile, Map.of(credentialPropertyId, "updated")); + Assertions.assertTrue(secrets.containsValue("updated")); + Assertions.assertFalse(secrets.containsValue("first")); + + WebAIProfileCredentials.saveCredentials(webSession, profile, Map.of(credentialPropertyId, "")); + Assertions.assertFalse(WebAIProfileCredentials.areCredentialsSaved(webSession, profile)); + } + + @Test + public void rejectsNonCredentialProperties() { + Assertions.assertThrows( + DBException.class, + () -> WebAIProfileCredentials.saveCredentials(webSession, profile, Map.of("model", "invalid")) + ); + } + + @Test + public void removesCredentialsFromNonGlobalConfiguration() throws DBException { + properties.setToken("global-token"); + + WebAIProfileCredentials.prepareGlobalProfile(webSession, profile); + + Assertions.assertNull(properties.getToken()); + } + + @Test + public void storesCredentialsInSessionWithoutPrivateSecretStorage() throws DBException { + Mockito.when(secretController.getSupportedFeatures()).thenReturn(0L); + + WebAIProfileCredentials.saveCredentials(webSession, profile, Map.of(credentialPropertyId, "session-token")); + + Assertions.assertTrue(WebAIProfileCredentials.areCredentialsSaved(webSession, profile)); + Assertions.assertTrue(secrets.isEmpty()); + + WebAIProfileCredentials.saveCredentials(webSession, profile, Map.of(credentialPropertyId, "")); + Assertions.assertFalse(WebAIProfileCredentials.areCredentialsSaved(webSession, profile)); + } +} diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java index 0f9379d9577..035f1d65f34 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java @@ -23,6 +23,7 @@ import io.cloudbeaver.model.rm.lock.RMLockTest; import io.cloudbeaver.model.session.WebSessionProjectTest; import io.cloudbeaver.model.session.WebSessionTest; +import io.cloudbeaver.service.ai.WebAIProfileCredentialsTest; import io.cloudbeaver.test.platform.admin.AdminCreateUserTest; import io.cloudbeaver.test.platform.admin.AdminImportUsersTest; import io.cloudbeaver.test.platform.admin.AdminLastLoginTimeTest; @@ -45,6 +46,7 @@ WebSessionTest.class, WebSessionProjectTest.class, WebNavigatorNodeInfoTest.class, + WebAIProfileCredentialsTest.class, AdminCreateUserTest.class, AdminImportUsersTest.class, AdminLastLoginTimeTest.class, From 44a7aaaffd10e211f40fa64fb9aff1fe86546e2a Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Wed, 2 Sep 2026 18:42:12 +0200 Subject: [PATCH 02/31] dbeaver/pro#9532 adds frontend for user-scoped profiles --- .../src/queries/ai/createAiProfile.gql | 1 + .../core-sdk/src/queries/ai/getAiProfiles.gql | 2 + .../queries/ai/saveAiProfileCredentials.gql | 3 + .../src/queries/ai/updateAiProfile.gql | 1 + .../src/AIAdministrationPage.tsx | 4 +- .../Options/AIProfileFormPart.ts | 35 ++++- .../Options/AIProfileOptions.tsx | 76 +++++++-- .../AIProfileForm/Options/AIProfileSchema.ts | 1 + .../Options/getAIProfileFormPart.ts | 4 +- .../src/AIProfiles/AIProfilesPanel.tsx | 4 +- .../src/AIProfiles/AIProfilesResource.ts | 18 ++- .../src/AIProfiles/AIProfilesTable.tsx | 7 +- .../src/AIProfiles/useAIProfilesTable.ts | 12 +- .../AdministrationAISettingsInfoPart.ts | 4 +- ...getAdministrationAISettingsFormInfoPart.ts | 4 +- .../src/locales/de.ts | 5 + .../src/locales/en.ts | 5 + .../src/locales/fr.ts | 5 + .../src/locales/ru.ts | 5 + .../plugin-ai-administration/src/module.ts | 6 +- .../AIChatConversationProfile.tsx | 63 ++++++-- .../AIChatConversationScope.tsx | 6 +- .../AIChatConversationsResource.ts | 22 ++- .../AIChatMessage/AIChatMessageService.ts | 35 ++++- .../packages/plugin-ai-chat/src/locales/en.ts | 2 + webapp/packages/plugin-ai-chat/src/module.ts | 4 +- .../plugin-ai-user-profile/.gitignore | 17 ++ .../plugin-ai-user-profile/package.json | 46 ++++++ .../src/AIUserProfileBootstrap.ts | 52 +++++++ .../src/LocaleService.ts | 33 ++++ .../src/components/AIProfilesPanel.tsx | 57 +++++++ .../src/components/AIProfilesTable.tsx | 109 +++++++++++++ .../plugin-ai-user-profile/src/index.ts | 9 ++ .../plugin-ai-user-profile/src/locales/en.ts | 25 +++ .../plugin-ai-user-profile/src/locales/fr.ts | 25 +++ .../plugin-ai-user-profile/src/locales/ru.ts | 25 +++ .../plugin-ai-user-profile/src/locales/zh.ts | 25 +++ .../plugin-ai-user-profile/src/module.ts | 22 +++ .../plugin-ai-user-profile/tsconfig.json | 51 ++++++ webapp/packages/plugin-ai/package.json | 5 + .../src/AIProfileCredentialsDialog.tsx | 146 ++++++++++++++++++ .../src/AIProfileCredentialsDialogLazy.ts | 13 ++ .../src/AIProfileCredentialsService.ts | 45 ++++++ .../src/AIProfileCredentialsUtils.ts | 21 +++ .../src/AISettingsResource.ts} | 24 +-- .../src/IAIProfileCredentialsDialogPayload.ts | 15 ++ .../packages/plugin-ai/src/LocaleService.ts | 34 ++++ .../plugin-ai/src/UserAIProfileResource.ts | 95 ++++++++++++ webapp/packages/plugin-ai/src/index.ts | 6 + webapp/packages/plugin-ai/src/locales/en.ts | 21 +++ webapp/packages/plugin-ai/src/locales/fr.ts | 21 +++ webapp/packages/plugin-ai/src/locales/ru.ts | 21 +++ webapp/packages/plugin-ai/src/locales/zh.ts | 21 +++ webapp/packages/plugin-ai/src/module.ts | 16 +- webapp/packages/plugin-ai/tsconfig.json | 15 ++ .../packages/plugin-set-common/package.json | 1 + .../packages/plugin-set-common/src/index.ts | 2 + .../packages/plugin-set-common/tsconfig.json | 3 + webapp/yarn.lock | 32 ++++ 59 files changed, 1297 insertions(+), 90 deletions(-) create mode 100644 webapp/packages/core-sdk/src/queries/ai/saveAiProfileCredentials.gql create mode 100644 webapp/packages/plugin-ai-user-profile/.gitignore create mode 100644 webapp/packages/plugin-ai-user-profile/package.json create mode 100644 webapp/packages/plugin-ai-user-profile/src/AIUserProfileBootstrap.ts create mode 100644 webapp/packages/plugin-ai-user-profile/src/LocaleService.ts create mode 100644 webapp/packages/plugin-ai-user-profile/src/components/AIProfilesPanel.tsx create mode 100644 webapp/packages/plugin-ai-user-profile/src/components/AIProfilesTable.tsx create mode 100644 webapp/packages/plugin-ai-user-profile/src/index.ts create mode 100644 webapp/packages/plugin-ai-user-profile/src/locales/en.ts create mode 100644 webapp/packages/plugin-ai-user-profile/src/locales/fr.ts create mode 100644 webapp/packages/plugin-ai-user-profile/src/locales/ru.ts create mode 100644 webapp/packages/plugin-ai-user-profile/src/locales/zh.ts create mode 100644 webapp/packages/plugin-ai-user-profile/src/module.ts create mode 100644 webapp/packages/plugin-ai-user-profile/tsconfig.json create mode 100644 webapp/packages/plugin-ai/src/AIProfileCredentialsDialog.tsx create mode 100644 webapp/packages/plugin-ai/src/AIProfileCredentialsDialogLazy.ts create mode 100644 webapp/packages/plugin-ai/src/AIProfileCredentialsService.ts create mode 100644 webapp/packages/plugin-ai/src/AIProfileCredentialsUtils.ts rename webapp/packages/{plugin-ai-chat/src/AIChatProfilesResource.ts => plugin-ai/src/AISettingsResource.ts} (57%) create mode 100644 webapp/packages/plugin-ai/src/IAIProfileCredentialsDialogPayload.ts create mode 100644 webapp/packages/plugin-ai/src/LocaleService.ts create mode 100644 webapp/packages/plugin-ai/src/UserAIProfileResource.ts create mode 100644 webapp/packages/plugin-ai/src/locales/en.ts create mode 100644 webapp/packages/plugin-ai/src/locales/fr.ts create mode 100644 webapp/packages/plugin-ai/src/locales/ru.ts create mode 100644 webapp/packages/plugin-ai/src/locales/zh.ts diff --git a/webapp/packages/core-sdk/src/queries/ai/createAiProfile.gql b/webapp/packages/core-sdk/src/queries/ai/createAiProfile.gql index cf85c3db2b1..b1c8cad1786 100644 --- a/webapp/packages/core-sdk/src/queries/ai/createAiProfile.gql +++ b/webapp/packages/core-sdk/src/queries/ai/createAiProfile.gql @@ -3,6 +3,7 @@ mutation createAiProfile($config: AIConfigurationProfileInput!) { id name engineId + global configuration { ...ObjectPropertyInfo } diff --git a/webapp/packages/core-sdk/src/queries/ai/getAiProfiles.gql b/webapp/packages/core-sdk/src/queries/ai/getAiProfiles.gql index 6bd5cfeb014..5b547411e34 100644 --- a/webapp/packages/core-sdk/src/queries/ai/getAiProfiles.gql +++ b/webapp/packages/core-sdk/src/queries/ai/getAiProfiles.gql @@ -3,5 +3,7 @@ query getAiProfiles { id name engineId + global + credentialsSaved } } diff --git a/webapp/packages/core-sdk/src/queries/ai/saveAiProfileCredentials.gql b/webapp/packages/core-sdk/src/queries/ai/saveAiProfileCredentials.gql new file mode 100644 index 00000000000..19058fed6f7 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/ai/saveAiProfileCredentials.gql @@ -0,0 +1,3 @@ +mutation saveAiProfileCredentials($profileId: ID!, $credentials: AIProfileCredentialsInput!) { + result: aiSaveProfileCredentials(profileId: $profileId, credentials: $credentials) +} diff --git a/webapp/packages/core-sdk/src/queries/ai/updateAiProfile.gql b/webapp/packages/core-sdk/src/queries/ai/updateAiProfile.gql index 5c23c6a2da8..9fbd3874cb3 100644 --- a/webapp/packages/core-sdk/src/queries/ai/updateAiProfile.gql +++ b/webapp/packages/core-sdk/src/queries/ai/updateAiProfile.gql @@ -3,6 +3,7 @@ mutation updateAiProfile($config: AIConfigurationProfileInput!) { id name engineId + global configuration { ...ObjectPropertyInfo } diff --git a/webapp/packages/plugin-ai-administration/src/AIAdministrationPage.tsx b/webapp/packages/plugin-ai-administration/src/AIAdministrationPage.tsx index a5ab7f1fd23..cad4a668f32 100644 --- a/webapp/packages/plugin-ai-administration/src/AIAdministrationPage.tsx +++ b/webapp/packages/plugin-ai-administration/src/AIAdministrationPage.tsx @@ -28,7 +28,7 @@ import { useService } from '@cloudbeaver/core-di'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; import { NotificationService } from '@cloudbeaver/core-events'; -import { AIProfilesResource } from './AIProfiles/AIProfilesResource.js'; +import { AIAdminProfilesResource } from './AIProfiles/AIProfilesResource.js'; import { getAdministrationAISettingsFormInfoPart } from './AISettingsForm/getAdministrationAISettingsFormInfoPart.js'; import { LANGUAGE_OPTIONS } from './AISettingsForm/getLanguageOptions.js'; import type { AdministrationAISettingsFormState } from './AISettingsForm/AdministrationAISettingsFormState.js'; @@ -41,7 +41,7 @@ export const AIAdministrationPage = observer<{ }>(function AIAdministrationPage({ formState }) { const translate = useTranslate(); const notificationService = useService(NotificationService); - const profilesLoader = useResource(AIAdministrationPage, AIProfilesResource, CachedMapAllKey); + const profilesLoader = useResource(AIAdministrationPage, AIAdminProfilesResource, CachedMapAllKey); const aiEnginesResource = useResource(AIAdministrationPage, AiEnginesResource, undefined); const profiles = profilesLoader.data.filter(isDefined); diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/Options/AIProfileFormPart.ts b/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/Options/AIProfileFormPart.ts index 069f7af6d2c..d1d7f75bcf9 100644 --- a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/Options/AIProfileFormPart.ts +++ b/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/Options/AIProfileFormPart.ts @@ -7,27 +7,32 @@ */ import { runInAction } from 'mobx'; -import { FormMode, FormPart, type IFormState } from '@cloudbeaver/core-ui'; +import { FormMode, FormPart, formValidationContext, type IFormState } from '@cloudbeaver/core-ui'; +import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; import type { AiEngineConfig } from '@cloudbeaver/core-sdk'; import { getUniqueName, trimObjectValues } from '@cloudbeaver/core-utils'; +import { supportsUserCredentials } from '@cloudbeaver/plugin-ai'; import { AIEnginePropertiesResource } from '../../AIEnginePropertiesResource.js'; -import { type AIAdminProfile, type AIProfileInput, AIProfilesResource } from '../../AIProfilesResource.js'; +import { AIAdminProfilesResource, type AIAdminProfile, type AIProfileInput } from '../../AIProfilesResource.js'; import { getObjectPropertiesValues } from '../../utils/getObjectPropertiesValues.js'; import { prepareProperties } from '../../utils/prepareProperties.js'; import type { IAIProfileFormState } from '../IAIProfileFormState.js'; import type { IAIProfileOptionsState } from './AIProfileSchema.js'; +const GLOBAL_PROPERTY_ID = 'global'; + const getDefaultState = (): IAIProfileOptionsState => ({ name: '', engineId: '', + global: true, properties: {}, }); export class AIProfileFormPart extends FormPart { constructor( formState: IFormState, - private readonly aiProfilesResource: AIProfilesResource, + private readonly aiProfilesResource: AIAdminProfilesResource, private readonly aiEnginePropertiesResource: AIEnginePropertiesResource, ) { super(formState, getDefaultState()); @@ -79,6 +84,11 @@ export class AIProfileFormPart extends FormPart { this.state.engineId = engineId; this.state.properties = getObjectPropertiesValues(propertiesInfo ?? []); + this.state.global = this.state.properties[GLOBAL_PROPERTY_ID] !== false; + this.state.properties[GLOBAL_PROPERTY_ID] = this.state.global; + if (!supportsUserCredentials(propertiesInfo ?? [])) { + this.state.global = true; + } }); } @@ -93,7 +103,15 @@ export class AIProfileFormPart extends FormPart, contexts: IExecutionContextProvider>): void { + const properties = this.aiEnginePropertiesResource.get(this.state.engineId) ?? []; + if (!this.state.global && !supportsUserCredentials(properties)) { + contexts.getContext(formValidationContext).error('plugin_ai_administration_profile_user_credentials_unsupported'); + } + } + private getConfig(): AIProfileInput { + this.state.properties[GLOBAL_PROPERTY_ID] = this.state.global; return { profileId: this.formState.state.profileId, profileName: this.state.name, @@ -102,7 +120,9 @@ export class AIProfileFormPart extends FormPart this.state.global || property.id !== 'token', + ), }), }, }; @@ -125,7 +145,8 @@ export class AIProfileFormPart extends FormPart = observer(function AIProfileOptions({ formState }) { const translate = useTranslate(); const notificationService = useService(NotificationService); - const aiProfilesResource = useService(AIProfilesResource); + const aiProfilesResource = useService(AIAdminProfilesResource); const enginesLoader = useResource(AIProfileOptions, AiEnginesResource, undefined); const part = getAIProfileFormPart(formState); const propertiesLoader = useResource(AIProfileOptions, AIEnginePropertiesResource, part.state.engineId || null); const propertiesInfo = propertiesLoader.data ?? []; + const usesUserCredentials = !part.state.global; + const configurableProperties = requireGlobalProfileToken( + propertiesInfo.filter(property => property.id !== 'global' && (!usesUserCredentials || property.id !== 'token')), + part.state.global, + ); const isEditMode = formState.mode === FormMode.Edit; const [isLoading, setIsLoading] = useState(false); const [models, setModels] = useState(null); @@ -68,12 +75,13 @@ export const AIProfileOptions: TabContainerPanelComponent = return null; }); - const modelPropertyIndex = propertiesInfo.findIndex(property => property.id === MODEL_PROPERTY_ID); - const modelProperty = propertiesInfo[modelPropertyIndex]; + const modelPropertyIndex = configurableProperties.findIndex(property => property.id === MODEL_PROPERTY_ID); + const modelProperty = configurableProperties[modelPropertyIndex]; const chatModels = (models ?? []).filter(model => model.features.map(feature => feature.toLowerCase()).includes('chat')); const hasModels = !!modelProperty; - const propertiesBeforeModel = hasModels ? propertiesInfo.slice(0, modelPropertyIndex) : propertiesInfo; - const propertiesAfterModel = hasModels ? propertiesInfo.slice(modelPropertyIndex + 1) : []; + const userCredentialsSupported = supportsUserCredentials(propertiesInfo); + const propertiesBeforeModel = hasModels ? configurableProperties.slice(0, modelPropertyIndex) : configurableProperties; + const propertiesAfterModel = hasModels ? configurableProperties.slice(modelPropertyIndex + 1) : []; function applyModelToProfile(modelId: string | null, availableModels = models ?? []): void { const model = availableModels.find(model => model.id === modelId); @@ -130,7 +138,7 @@ export const AIProfileOptions: TabContainerPanelComponent = executor: formState.loadedTask, handlers: [ async () => { - if (isEditMode && part.state.engineId && models === null) { + if (isEditMode && part.state.engineId && models === null && !usesUserCredentials) { await loadModels(false); } }, @@ -146,6 +154,15 @@ export const AIProfileOptions: TabContainerPanelComponent = setModels(null); } + function handleProfileTypeChange(value: string): void { + const global = value === 'global'; + part.state.global = global; + part.state.properties['global'] = global; + if (!global) { + part.state.properties['token'] = null; + } + } + return ( @@ -167,6 +184,25 @@ export const AIProfileOptions: TabContainerPanelComponent = > {translate('plugin_ai_administration_profile_form_field_engine')} + + + {translate('plugin_ai_administration_profile_global_credentials')} + + + {translate('plugin_ai_administration_profile_user_credentials')} + + {!!part.state.engineId && ( @@ -196,17 +232,23 @@ export const AIProfileOptions: TabContainerPanelComponent = > {translate('ai_administration_select_language_model_selector_title')} -
- -
+ {!usesUserCredentials && ( +
+ +
+ )} )} - + )} diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/Options/AIProfileSchema.ts b/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/Options/AIProfileSchema.ts index fe86a0faa88..643daf8baeb 100644 --- a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/Options/AIProfileSchema.ts +++ b/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/Options/AIProfileSchema.ts @@ -14,6 +14,7 @@ export const AI_PROFILE_NAME_MAX_LENGTH = 100; export const AIProfileSchema = schema.object({ name: schema.string().min(AI_PROFILE_NAME_MIN_LENGTH).max(AI_PROFILE_NAME_MAX_LENGTH), engineId: schema.string().min(1), + global: schema.boolean(), properties: schema.record(schema.string(), schema.any()), }); diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/Options/getAIProfileFormPart.ts b/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/Options/getAIProfileFormPart.ts index 84e1c3f1335..ed4ab3914e6 100644 --- a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/Options/getAIProfileFormPart.ts +++ b/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/Options/getAIProfileFormPart.ts @@ -9,7 +9,7 @@ import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-d import type { IFormState } from '@cloudbeaver/core-ui'; import { AIEnginePropertiesResource } from '../../AIEnginePropertiesResource.js'; -import { AIProfilesResource } from '../../AIProfilesResource.js'; +import { AIAdminProfilesResource } from '../../AIProfilesResource.js'; import type { IAIProfileFormState } from '../IAIProfileFormState.js'; import { AIProfileFormPart } from './AIProfileFormPart.js'; @@ -18,7 +18,7 @@ const DATA_CONTEXT_AI_PROFILE_FORM_PART = createDataContext(' export function getAIProfileFormPart(formState: IFormState): AIProfileFormPart { return formState.getPart(DATA_CONTEXT_AI_PROFILE_FORM_PART, context => { const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; - const aiProfilesResource = di.getService(AIProfilesResource); + const aiProfilesResource = di.getService(AIAdminProfilesResource); const aiEnginePropertiesResource = di.getService(AIEnginePropertiesResource); return new AIProfileFormPart(formState, aiProfilesResource, aiEnginePropertiesResource); diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesPanel.tsx b/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesPanel.tsx index a7f4e9085b7..a57a1049aa5 100644 --- a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesPanel.tsx +++ b/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesPanel.tsx @@ -30,7 +30,7 @@ import { isDefined } from '@dbeaver/js-helpers'; import type { AdministrationAISettingsFormState } from '../AISettingsForm/AdministrationAISettingsFormState.js'; import { getAdministrationAISettingsFormInfoPart } from '../AISettingsForm/getAdministrationAISettingsFormInfoPart.js'; import { AIProfileFormService } from './AIProfileForm/AIProfileFormService.js'; -import { AIProfilesResource } from './AIProfilesResource.js'; +import { AIAdminProfilesResource } from './AIProfilesResource.js'; import AIProfilesToolsPanelStyles from './AIProfilesToolsPanel.module.css'; import { AIProfilesTable } from './AIProfilesTable.js'; import { useAIProfilesTable } from './useAIProfilesTable.js'; @@ -51,7 +51,7 @@ export const AIProfilesPanel = observer(function AIProfilesPanel({ formSt const settingsInfoPart = getAdministrationAISettingsFormInfoPart(formState); useAutoLoad(AIProfilesPanel, settingsInfoPart); - const profilesLoader = useResource(AIProfilesPanel, AIProfilesResource, CachedMapAllKey); + const profilesLoader = useResource(AIProfilesPanel, AIAdminProfilesResource, CachedMapAllKey); const profiles = profilesLoader.data.filter(isDefined); const defaultProfileId = settingsInfoPart.initialState.defaultConfiguration; diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesResource.ts b/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesResource.ts index db50411edfd..55a444348be 100644 --- a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesResource.ts +++ b/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesResource.ts @@ -12,24 +12,24 @@ import { GraphQLService, type AiEngineConfig, type AiAdminConfigurationProfileInfo, - type AiConfigurationProfileInfo, type AiConfigurationProfileInput, type AiModelInfo, } from '@cloudbeaver/core-sdk'; +import { type AIProfile, UserAIProfileResource } from '@cloudbeaver/plugin-ai'; import { AISettingsResource } from '../AISettingsResource.js'; -export type AIProfile = AiConfigurationProfileInfo; export type AIAdminProfile = AiAdminConfigurationProfileInfo; export type AIProfileInput = AiConfigurationProfileInput; -@injectable(() => [GraphQLService, SessionPermissionsResource, ServerConfigResource, AISettingsResource]) -export class AIProfilesResource extends CachedMapResource { +@injectable(() => [GraphQLService, SessionPermissionsResource, ServerConfigResource, AISettingsResource, UserAIProfileResource]) +export class AIAdminProfilesResource extends CachedMapResource { constructor( private readonly graphQLService: GraphQLService, permissionsResource: SessionPermissionsResource, serverConfigResource: ServerConfigResource, aiSettingsResource: AISettingsResource, + private readonly userAIProfileResource: UserAIProfileResource, ) { super(); @@ -48,7 +48,8 @@ export class AIProfilesResource extends CachedMapResource { async create(config: AIProfileInput): Promise { const { profile } = await this.graphQLService.sdk.createAiProfile({ config }); - this.set(profile.id, profile); + this.userAIProfileResource.setProfile(profile); + this.set(profile.id, this.userAIProfileResource.get(profile.id)!); return profile; } @@ -56,7 +57,8 @@ export class AIProfilesResource extends CachedMapResource { async update(config: AIProfileInput): Promise { const { profile } = await this.graphQLService.sdk.updateAiProfile({ config }); - this.set(profile.id, profile); + this.userAIProfileResource.setProfile(profile); + this.set(profile.id, this.userAIProfileResource.get(profile.id)!); return profile; } @@ -64,6 +66,7 @@ export class AIProfilesResource extends CachedMapResource { async deleteProfile(profileId: string): Promise { await this.graphQLService.sdk.deleteAiProfile({ profileId }); + this.userAIProfileResource.removeProfile(profileId); this.delete(profileId); } @@ -73,7 +76,8 @@ export class AIProfilesResource extends CachedMapResource { } protected async loader(): Promise> { - const { profiles } = await this.graphQLService.sdk.getAiProfiles(); + await this.userAIProfileResource.refresh(CachedMapAllKey); + const profiles = this.userAIProfileResource.values; const key = resourceKeyList(profiles.map(profile => profile.id)); this.replace(key, profiles); diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesTable.tsx b/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesTable.tsx index 00d71b5add7..f6ed46a2ba4 100644 --- a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesTable.tsx +++ b/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesTable.tsx @@ -13,12 +13,10 @@ import { IconOrImage, Link, s, TextPlaceholder, useResource, useS, useTranslate import { useService } from '@cloudbeaver/core-di'; import { ADMINISTRATION_TABLE_DEFAULT_ROW_HEIGHT, AdministrationTableStyles } from '@cloudbeaver/core-administration'; import { DataGrid, TableRowSelect, useCreateGridReactiveValue } from '@cloudbeaver/plugin-data-grid'; -import { AiEnginesResource } from '@cloudbeaver/plugin-ai'; +import { AiEnginesResource, type AIProfile } from '@cloudbeaver/plugin-ai'; import { Command } from '@dbeaver/ui-kit'; import { AIProfileFormService } from './AIProfileForm/AIProfileFormService.js'; -import type { AIProfile } from './AIProfilesResource.js'; - interface Props { profiles: AIProfile[]; defaultProfileId: string | null; @@ -78,6 +76,7 @@ export const AIProfilesTable = observer(function AIProfilesTable({ profil {isDefault && ( {translate('plugin_ai_administration_profile_default_badge')} )} + {profile.global && } ); } @@ -89,8 +88,8 @@ export const AIProfilesTable = observer(function AIProfilesTable({ profil if (engine?.icon) { return (
- {title} +
); } diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/useAIProfilesTable.ts b/webapp/packages/plugin-ai-administration/src/AIProfiles/useAIProfilesTable.ts index ecac4fca5d9..ca2a79eb719 100644 --- a/webapp/packages/plugin-ai-administration/src/AIProfiles/useAIProfilesTable.ts +++ b/webapp/packages/plugin-ai-administration/src/AIProfiles/useAIProfilesTable.ts @@ -15,11 +15,11 @@ import { NotificationService } from '@cloudbeaver/core-events'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; import type { ITableSelection } from '@cloudbeaver/plugin-data-grid'; -import { AIProfilesResource } from './AIProfilesResource.js'; +import { AIAdminProfilesResource } from './AIProfilesResource.js'; interface State { processing: boolean; - aiProfilesResource: AIProfilesResource; + aiProfilesResource: AIAdminProfilesResource; notificationService: NotificationService; dialogService: CommonDialogService; selection: ITableSelection; @@ -30,7 +30,7 @@ interface State { export function useAIProfilesTable(selection: ITableSelection): Readonly { const notificationService = useService(NotificationService); const dialogService = useService(CommonDialogService); - const aiProfilesResource = useService(AIProfilesResource); + const aiProfilesResource = useService(AIAdminProfilesResource); const translate = useTranslate(); return useObservableRef( @@ -63,7 +63,11 @@ export function useAIProfilesTable(selection: ITableSelection): Readonly } const names = deletionList.map(id => `"${this.aiProfilesResource.get(id)?.name ?? id}"`).join(', '); - const message = `${translate('plugin_ai_administration_profile_delete_confirmation')}${names}.\n\n${translate('ui_are_you_sure')}`; + const deletesUserCredentials = deletionList.some(id => this.aiProfilesResource.get(id)?.global === false); + const credentialsWarning = deletesUserCredentials + ? `\n\n${translate('plugin_ai_administration_profile_delete_user_credentials_warning')}` + : ''; + const message = `${translate('plugin_ai_administration_profile_delete_confirmation')}${names}.${credentialsWarning}\n\n${translate('ui_are_you_sure')}`; const { status } = await this.dialogService.open(ConfirmationDialogDelete, { title: 'ui_data_delete_confirmation', diff --git a/webapp/packages/plugin-ai-administration/src/AISettingsForm/AdministrationAISettingsInfoPart.ts b/webapp/packages/plugin-ai-administration/src/AISettingsForm/AdministrationAISettingsInfoPart.ts index e1a928ec5a0..51248b36766 100644 --- a/webapp/packages/plugin-ai-administration/src/AISettingsForm/AdministrationAISettingsInfoPart.ts +++ b/webapp/packages/plugin-ai-administration/src/AISettingsForm/AdministrationAISettingsInfoPart.ts @@ -9,7 +9,7 @@ import { CachedMapAllKey } from '@cloudbeaver/core-resource'; import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { FormPart, formValidationContext, type IFormState } from '@cloudbeaver/core-ui'; -import { AIProfilesResource } from '../AIProfiles/AIProfilesResource.js'; +import { AIAdminProfilesResource } from '../AIProfiles/AIProfilesResource.js'; import type { AISettingsResource } from '../AISettingsResource.js'; import { LANGUAGE_VALIDATION_REGEX } from './getLanguageOptions.js'; import type { IAdministrationAIInfoState } from './IAdministrationAIInfoState.js'; @@ -23,7 +23,7 @@ export class AdministrationAISettingsInfoPart extends FormPart, private readonly aiSettingsResource: AISettingsResource, - private readonly aiProfilesResource: AIProfilesResource, + private readonly aiProfilesResource: AIAdminProfilesResource, ) { super(formState, DEFAULT_STATE_GETTER()); } diff --git a/webapp/packages/plugin-ai-administration/src/AISettingsForm/getAdministrationAISettingsFormInfoPart.ts b/webapp/packages/plugin-ai-administration/src/AISettingsForm/getAdministrationAISettingsFormInfoPart.ts index 17767f712a7..05f94304b06 100644 --- a/webapp/packages/plugin-ai-administration/src/AISettingsForm/getAdministrationAISettingsFormInfoPart.ts +++ b/webapp/packages/plugin-ai-administration/src/AISettingsForm/getAdministrationAISettingsFormInfoPart.ts @@ -8,7 +8,7 @@ import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; import type { IFormState } from '@cloudbeaver/core-ui'; -import { AIProfilesResource } from '../AIProfiles/AIProfilesResource.js'; +import { AIAdminProfilesResource } from '../AIProfiles/AIProfilesResource.js'; import { AISettingsResource } from '../AISettingsResource.js'; import { AdministrationAISettingsInfoPart } from './AdministrationAISettingsInfoPart.js'; @@ -20,7 +20,7 @@ export function getAdministrationAISettingsFormInfoPart(formState: IFormState { const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; const aiSettingsResource = di.getService(AISettingsResource); - const aiProfilesResource = di.getService(AIProfilesResource); + const aiProfilesResource = di.getService(AIAdminProfilesResource); return new AdministrationAISettingsInfoPart(formState, aiSettingsResource, aiProfilesResource); }); diff --git a/webapp/packages/plugin-ai-administration/src/locales/de.ts b/webapp/packages/plugin-ai-administration/src/locales/de.ts index 093afdb8449..61eef398ac1 100644 --- a/webapp/packages/plugin-ai-administration/src/locales/de.ts +++ b/webapp/packages/plugin-ai-administration/src/locales/de.ts @@ -36,6 +36,7 @@ export default [ ['plugin_ai_administration_profiles_refresh_success', 'Profilliste aktualisiert'], ['plugin_ai_administration_profiles_refresh_error', 'Profilliste konnte nicht aktualisiert werden'], ['plugin_ai_administration_profile_delete_confirmation', 'Sie sind dabei, folgende Profile zu löschen: '], + ['plugin_ai_administration_profile_delete_user_credentials_warning', 'Gespeicherte Anmeldedaten für alle Benutzer dieser Profile werden ebenfalls gelöscht.'], ['plugin_ai_administration_profile_delete_success', 'Ausgewählte Profile gelöscht'], ['plugin_ai_administration_profile_delete_error', 'Profile konnten nicht gelöscht werden'], ['plugin_ai_administration_profile_created', 'Profil erstellt'], @@ -44,6 +45,10 @@ export default [ ['plugin_ai_administration_profile_save_error', 'Profil konnte nicht gespeichert werden'], ['plugin_ai_administration_profile_form_field_name', 'Profilname'], ['plugin_ai_administration_profile_form_field_engine', 'Engine'], + ['plugin_ai_administration_profile_profile_type', 'Quelle der Anmeldedaten'], + ['plugin_ai_administration_profile_global_credentials', 'Globale Anmeldedaten'], + ['plugin_ai_administration_profile_user_credentials', 'Benutzeranmeldedaten'], + ['plugin_ai_administration_profile_user_credentials_unsupported', 'Diese Engine unterstützt keine vom Benutzer bereitgestellten API-Token'], ['plugin_ai_administration_profile_form_tab_options', 'Profil'], ['plugin_ai_administration_profile_name_max_length', 'Der Profilname darf {arg:length} Zeichen nicht überschreiten'], ['plugin_ai_administration_profile_name_min_length', 'Der Profilname muss mindestens {arg:length} Zeichen lang sein'], diff --git a/webapp/packages/plugin-ai-administration/src/locales/en.ts b/webapp/packages/plugin-ai-administration/src/locales/en.ts index 437e75f1448..f1480a933a8 100644 --- a/webapp/packages/plugin-ai-administration/src/locales/en.ts +++ b/webapp/packages/plugin-ai-administration/src/locales/en.ts @@ -36,6 +36,7 @@ export default [ ['plugin_ai_administration_profiles_refresh_success', 'Profiles list updated'], ['plugin_ai_administration_profiles_refresh_error', 'Failed to refresh profiles list'], ['plugin_ai_administration_profile_delete_confirmation', 'You are about to delete profile(s): '], + ['plugin_ai_administration_profile_delete_user_credentials_warning', 'Saved credentials for all users of these profiles will also be deleted.'], ['plugin_ai_administration_profile_delete_success', 'Selected profiles deleted'], ['plugin_ai_administration_profile_delete_error', 'Failed to delete profiles'], ['plugin_ai_administration_profile_created', 'Profile created'], @@ -44,6 +45,10 @@ export default [ ['plugin_ai_administration_profile_save_error', 'Failed to save profile'], ['plugin_ai_administration_profile_form_field_name', 'Profile name'], ['plugin_ai_administration_profile_form_field_engine', 'Engine'], + ['plugin_ai_administration_profile_profile_type', 'Credential source'], + ['plugin_ai_administration_profile_global_credentials', 'Global credentials'], + ['plugin_ai_administration_profile_user_credentials', 'User credentials'], + ['plugin_ai_administration_profile_user_credentials_unsupported', 'This engine does not support user-provided API tokens'], ['plugin_ai_administration_profile_form_tab_options', 'Profile'], ['plugin_ai_administration_profile_name_max_length', 'Profile name must not exceed {arg:length} characters'], ['plugin_ai_administration_profile_name_min_length', 'Profile name must be at least {arg:length} characters'], diff --git a/webapp/packages/plugin-ai-administration/src/locales/fr.ts b/webapp/packages/plugin-ai-administration/src/locales/fr.ts index 258889359a0..b66643d52b2 100644 --- a/webapp/packages/plugin-ai-administration/src/locales/fr.ts +++ b/webapp/packages/plugin-ai-administration/src/locales/fr.ts @@ -36,6 +36,7 @@ export default [ ['plugin_ai_administration_profiles_refresh_success', 'Liste des profils mise à jour'], ['plugin_ai_administration_profiles_refresh_error', 'Échec de l’actualisation de la liste des profils'], ['plugin_ai_administration_profile_delete_confirmation', 'Vous êtes sur le point de supprimer le(s) profil(s) : '], + ['plugin_ai_administration_profile_delete_user_credentials_warning', 'Les identifiants enregistrés pour tous les utilisateurs de ces profils seront également supprimés.'], ['plugin_ai_administration_profile_delete_success', 'Profils sélectionnés supprimés'], ['plugin_ai_administration_profile_delete_error', 'Échec de la suppression des profils'], ['plugin_ai_administration_profile_created', 'Profil créé'], @@ -44,6 +45,10 @@ export default [ ['plugin_ai_administration_profile_save_error', 'Échec de l’enregistrement du profil'], ['plugin_ai_administration_profile_form_field_name', 'Nom du profil'], ['plugin_ai_administration_profile_form_field_engine', "Modèle d'IA"], + ['plugin_ai_administration_profile_profile_type', 'Source des identifiants'], + ['plugin_ai_administration_profile_global_credentials', 'Identifiants globaux'], + ['plugin_ai_administration_profile_user_credentials', 'Identifiants utilisateur'], + ['plugin_ai_administration_profile_user_credentials_unsupported', 'Ce moteur ne prend pas en charge les jetons API fournis par les utilisateurs'], ['plugin_ai_administration_profile_form_tab_options', 'Profil'], ['plugin_ai_administration_profile_name_max_length', 'Le nom du profil ne doit pas dépasser {arg:length} caractères'], ['plugin_ai_administration_profile_name_min_length', 'Le nom du profil doit contenir au moins {arg:length} caractères'], diff --git a/webapp/packages/plugin-ai-administration/src/locales/ru.ts b/webapp/packages/plugin-ai-administration/src/locales/ru.ts index 7c9c453c76c..e90981343d9 100644 --- a/webapp/packages/plugin-ai-administration/src/locales/ru.ts +++ b/webapp/packages/plugin-ai-administration/src/locales/ru.ts @@ -36,6 +36,7 @@ export default [ ['plugin_ai_administration_profiles_refresh_success', 'Список профилей обновлён'], ['plugin_ai_administration_profiles_refresh_error', 'Не удалось обновить список профилей'], ['plugin_ai_administration_profile_delete_confirmation', 'Вы собираетесь удалить профили(ь): '], + ['plugin_ai_administration_profile_delete_user_credentials_warning', 'Сохраненные учетные данные всех пользователей этих профилей также будут удалены.'], ['plugin_ai_administration_profile_delete_success', 'Выбранные профили удалены'], ['plugin_ai_administration_profile_delete_error', 'Не удалось удалить профили'], ['plugin_ai_administration_profile_created', 'Профиль создан'], @@ -44,6 +45,10 @@ export default [ ['plugin_ai_administration_profile_save_error', 'Не удалось сохранить профиль'], ['plugin_ai_administration_profile_form_field_name', 'Название профиля'], ['plugin_ai_administration_profile_form_field_engine', 'Энджин'], + ['plugin_ai_administration_profile_profile_type', 'Источник учетных данных'], + ['plugin_ai_administration_profile_global_credentials', 'Глобальные учетные данные'], + ['plugin_ai_administration_profile_user_credentials', 'Учетные данные пользователя'], + ['plugin_ai_administration_profile_user_credentials_unsupported', 'Этот движок не поддерживает API-токены, предоставляемые пользователями'], ['plugin_ai_administration_profile_form_tab_options', 'Профиль'], ['plugin_ai_administration_profile_name_max_length', 'Название профиля не должно превышать {arg:length} символов'], ['plugin_ai_administration_profile_name_min_length', 'Название профиля должно содержать не менее {arg:length} символов'], diff --git a/webapp/packages/plugin-ai-administration/src/module.ts b/webapp/packages/plugin-ai-administration/src/module.ts index 01025028b65..dbb38d7fccf 100644 --- a/webapp/packages/plugin-ai-administration/src/module.ts +++ b/webapp/packages/plugin-ai-administration/src/module.ts @@ -11,7 +11,7 @@ import { LocaleService } from './LocaleService.js'; import { AISettingsResource } from './AISettingsResource.js'; import { AISettingsService } from './AISettingsService.js'; import { AIEnginePropertiesResource } from './AIProfiles/AIEnginePropertiesResource.js'; -import { AIProfilesResource } from './AIProfiles/AIProfilesResource.js'; +import { AIAdminProfilesResource } from './AIProfiles/AIProfilesResource.js'; import { AIProfileFormService } from './AIProfiles/AIProfileForm/AIProfileFormService.js'; import { AIProfileFormTabBootstrap } from './AIProfiles/AIProfileForm/AIProfileFormTabBootstrap.js'; import { AdministrationAISettingsFormService } from './AISettingsForm/AdministrationAISettingsFormService.js'; @@ -29,11 +29,11 @@ export default ModuleRegistry.add({ .addSingleton(Bootstrap, LocaleService) .addSingleton(Bootstrap, proxy(AIAdministrationTabsService)) .addSingleton(Dependency, proxy(AISettingsResource)) - .addSingleton(Dependency, proxy(AIProfilesResource)) + .addSingleton(Dependency, proxy(AIAdminProfilesResource)) .addSingleton(Dependency, proxy(AIEnginePropertiesResource)) .addSingleton(AISettingsResource) .addSingleton(AISettingsService) - .addSingleton(AIProfilesResource) + .addSingleton(AIAdminProfilesResource) .addSingleton(AIEnginePropertiesResource) .addSingleton(AIProfileFormService) .addSingleton(Bootstrap, AIProfileFormTabBootstrap) diff --git a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationScope/AIChatConversationProfile.tsx b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationScope/AIChatConversationProfile.tsx index 659edddad58..a34b3689eac 100644 --- a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationScope/AIChatConversationProfile.tsx +++ b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationScope/AIChatConversationProfile.tsx @@ -8,19 +8,19 @@ import { observer } from 'mobx-react-lite'; -import { MenuGroup, MenuGroupLabel, MenuItemRadio } from '@dbeaver/ui-kit'; -import { IconOrImage, RadioIndicator, useResource, useTranslate } from '@cloudbeaver/core-blocks'; +import { MenuGroup, MenuGroupLabel, MenuItemRadio, useMenuContext } from '@dbeaver/ui-kit'; +import { ActionIconButton, IconOrImage, RadioIndicator, useResource, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; +import { DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; -import { AiEnginesResource } from '@cloudbeaver/plugin-ai'; +import { AIProfileCredentialsDialogService, AiEnginesResource, requiresUserCredentials, type AIProfile } from '@cloudbeaver/plugin-ai'; -import type { AIChatProfile } from '../../../AIChatProfilesResource.js'; import type { AIChatConversationInfo } from '../AIChatConversationsResource.js'; import { AIChatConversationsService } from '../AIChatConversationsService.js'; interface Props { conversation: AIChatConversationInfo; - profiles: AIChatProfile[]; + profiles: AIProfile[]; disabled?: boolean; } @@ -28,17 +28,36 @@ export const AIChatConversationProfile = observer(function AIChatConversa const translate = useTranslate(); const notificationService = useService(NotificationService); const aiChatConversationsService = useService(AIChatConversationsService); + const credentialsDialogService = useService(AIProfileCredentialsDialogService); + const menu = useMenuContext(); const aiEnginesResource = useResource(AIChatConversationProfile, AiEnginesResource, undefined); - async function selectProfile(profileId: string) { + async function selectProfile(profile: AIProfile) { try { - await aiChatConversationsService.updateConversationProfile(conversation.id, profileId); + if (requiresUserCredentials(profile)) { + menu?.hide(); + const { status } = await credentialsDialogService.open(profile.id); + if (status !== DialogueStateResult.Resolved) { + return; + } + } + await aiChatConversationsService.updateConversationProfile(conversation.id, profile.id); } catch (exception: any) { notificationService.logException(exception, 'plugin_ai_chat_profile_change_fail'); } } + async function editCredentials(event: React.MouseEvent, profileId: string): Promise { + event.stopPropagation(); + menu?.hide(); + try { + await credentialsDialogService.open(profileId); + } catch (exception: any) { + notificationService.logException(exception, 'plugin_ai_chat_profile_credentials_edit_fail'); + } + } + if (profiles.length === 0) { return null; } @@ -54,20 +73,36 @@ export const AIChatConversationProfile = observer(function AIChatConversa return ( selectProfile(profile.id)} + onClick={() => selectProfile(profile)} > -
- - {engine?.icon && } - {profile.name} +
+
+ + {engine?.icon && } + {profile.name} +
+ {profile.global ? ( +
+ +
+ ) : ( + editCredentials(event, profile.id)} + /> + )}
); diff --git a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationScope/AIChatConversationScope.tsx b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationScope/AIChatConversationScope.tsx index 98882378969..dffabcabe4e 100644 --- a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationScope/AIChatConversationScope.tsx +++ b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationScope/AIChatConversationScope.tsx @@ -15,11 +15,12 @@ import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dial import { useService } from '@cloudbeaver/core-di'; import { ConnectionsManagerService, ContainerResource } from '@cloudbeaver/core-connections'; import { NotificationService } from '@cloudbeaver/core-events'; +import { CachedMapAllKey } from '@cloudbeaver/core-resource'; import { AiDatabaseScope } from '@cloudbeaver/core-sdk'; +import { UserAIProfileResource } from '@cloudbeaver/plugin-ai'; import { AIChatConversationScopeCustomDialog } from './AIChatConversationScopeCustom/AIChatConversationScopeCustomDialog.js'; import { AIChatConversationProfile } from './AIChatConversationProfile.js'; -import { AIChatProfilesResource } from '../../../AIChatProfilesResource.js'; import type { AIChatConversationInfo } from '../AIChatConversationsResource.js'; import { AIChatConversationsService } from '../AIChatConversationsService.js'; import { AIChatConversationScopeResource } from '../AIChatConversationScopeResource.js'; @@ -45,7 +46,8 @@ export const AIChatConversationScope = observer(function AIChatConversati const { data: container } = useResource(AIChatConversationScope, ContainerResource, conversation.dataSourceId ?? null); const { data: currentScope } = useResource(AIChatConversationScope, AIChatConversationScopeResource, conversation.id); - const { data: profiles } = useResource(AIChatConversationScope, AIChatProfilesResource, undefined); + const { data: profileList } = useResource(AIChatConversationScope, UserAIProfileResource, CachedMapAllKey); + const profiles = profileList.filter(profile => profile !== undefined); async function selectScope(scope: AiDatabaseScope) { if (!conversation.dataSourceId) { diff --git a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationsResource.ts b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationsResource.ts index 2d89ce9f452..8b2562dcc86 100644 --- a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationsResource.ts +++ b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationsResource.ts @@ -18,6 +18,7 @@ import { type ResourceKey, } from '@cloudbeaver/core-resource'; import { type AiChatConversationFragment, type AiChatConversationInput, GraphQLService } from '@cloudbeaver/core-sdk'; +import { AISettingsResource, UserAIProfileResource } from '@cloudbeaver/plugin-ai'; import type { EAIConversationPromptGeneratorId } from '../../EAIConversationPromptGeneratorId.js'; @@ -32,11 +33,13 @@ export const ChatConversationConnectionKey = resourceKeyListAliasFactory( }), ); -@injectable(() => [GraphQLService, UserInfoResource]) +@injectable(() => [GraphQLService, UserInfoResource, UserAIProfileResource, AISettingsResource]) export class AIChatConversationsResource extends CachedMapResource { constructor( private readonly graphQLService: GraphQLService, userInfoResource: UserInfoResource, + userAIProfileResource: UserAIProfileResource, + aiSettingsResource: AISettingsResource, ) { super(); @@ -44,6 +47,23 @@ export class AIChatConversationsResource extends CachedMapResource { + const deletedProfileIds = ResourceKeyUtils.toArray(key); + const conversations = this.values.filter(conversation => conversation.profile && deletedProfileIds.includes(conversation.profile)); + if (conversations.length === 0) { + return; + } + + const defaultProfileId = (await aiSettingsResource.load())?.defaultConfiguration; + if (!defaultProfileId || deletedProfileIds.includes(defaultProfileId)) { + return; + } + + await Promise.all( + conversations.map(conversation => this.updateConversation(conversation.id, { settings: { profile: defaultProfileId } })), + ); + }); + this.aliases.add(ChatConversationConnectionKey, param => resourceKeyList( this.values diff --git a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/AIChatMessageService.ts b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/AIChatMessageService.ts index 5a96bef73df..232ab2e7b79 100644 --- a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/AIChatMessageService.ts +++ b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/AIChatMessageService.ts @@ -15,6 +15,7 @@ import { LocalizationService } from '@cloudbeaver/core-localization'; import { Executor, ExecutorInterrupter } from '@cloudbeaver/core-executor'; import { ConnectionsManagerService, type IConnectionInfoParams } from '@cloudbeaver/core-connections'; import type { AiSendChatMessageInfoFragment } from '@cloudbeaver/core-sdk'; +import { AIProfileCredentialsDialogService, AISettingsResource, requiresUserCredentials, UserAIProfileResource } from '@cloudbeaver/plugin-ai'; import { AIChatMessagesResource, isFunctionConfirmationMessage, isFunctionMessage, type IMessageParam } from './AIChatMessagesResource.js'; import { AIChatConversationsResource } from '../AIChatConversation/AIChatConversationsResource.js'; @@ -44,7 +45,16 @@ interface IMessageSendExecutorAfterData { type MessageSendExecutorData = IMessageSendExecutorBeforeData | IMessageSendExecutorAfterData; -@injectable(() => [AIChatMessagesResource, AIChatConversationsResource, CommonDialogService, LocalizationService, ConnectionsManagerService]) +@injectable(() => [ + AIChatMessagesResource, + AIChatConversationsResource, + CommonDialogService, + LocalizationService, + ConnectionsManagerService, + UserAIProfileResource, + AISettingsResource, + AIProfileCredentialsDialogService, +]) export class AIChatMessageService { onMessageSend: Executor; @@ -54,6 +64,9 @@ export class AIChatMessageService { private readonly commonDialogService: CommonDialogService, private readonly localizationService: LocalizationService, private readonly connectionsManagerService: ConnectionsManagerService, + private readonly userAIProfileResource: UserAIProfileResource, + private readonly aiSettingsResource: AISettingsResource, + private readonly credentialsDialogService: AIProfileCredentialsDialogService, ) { this.onMessageSend = new Executor(); @@ -114,6 +127,26 @@ export class AIChatMessageService { async processSendMessageAction(conversationId: string, action: () => Promise) { const conversation = await this.aiChatConversationsResource.load(conversationId); + const settings = await this.aiSettingsResource.load(); + let profileId = conversation.profile ?? settings?.defaultConfiguration; + let profile = profileId ? await this.userAIProfileResource.load(profileId) : undefined; + + if (!profile && conversation.profile && settings?.defaultConfiguration && conversation.profile !== settings.defaultConfiguration) { + profileId = settings.defaultConfiguration; + profile = await this.userAIProfileResource.load(profileId); + if (profile) { + await this.aiChatConversationsResource.updateConversation(conversation.id, { settings: { profile: profileId } }); + } + } + + if (profileId) { + if (profile && requiresUserCredentials(profile)) { + const { status } = await this.credentialsDialogService.open(profile.id); + if (status !== DialogueStateResult.Resolved) { + return; + } + } + } const contexts = await this.onMessageSend.execute({ stage: 'before', data: { conversationId, connectionKey: conversation.dataSourceId } }); if (ExecutorInterrupter.isInterrupted(contexts)) { diff --git a/webapp/packages/plugin-ai-chat/src/locales/en.ts b/webapp/packages/plugin-ai-chat/src/locales/en.ts index 4dc15459e33..a86dba2c26b 100644 --- a/webapp/packages/plugin-ai-chat/src/locales/en.ts +++ b/webapp/packages/plugin-ai-chat/src/locales/en.ts @@ -50,6 +50,8 @@ export default [ ['plugin_ai_chat_scope_change_fail', 'Failed to change context'], ['plugin_ai_chat_profile_group', 'Active configuration'], ['plugin_ai_chat_profile_change_fail', 'Failed to change AI profile'], + ['plugin_ai_chat_profile_edit_credentials', 'Edit credentials'], + ['plugin_ai_chat_profile_credentials_edit_fail', 'Failed to edit AI profile credentials'], ['plugin_ai_chat_scope_connection', 'Connection'], ['plugin_ai_chat_scope_schema', 'Current Schema'], ['plugin_ai_chat_scope_database', 'Current Database'], diff --git a/webapp/packages/plugin-ai-chat/src/module.ts b/webapp/packages/plugin-ai-chat/src/module.ts index 7a68cfd4c12..99563334228 100644 --- a/webapp/packages/plugin-ai-chat/src/module.ts +++ b/webapp/packages/plugin-ai-chat/src/module.ts @@ -23,7 +23,6 @@ import { AIChatConversationScopeResource } from './AIChat/AIChatConversation/AIC import { AIChatConversationMetricsResource } from './AIChat/AIChatConversation/AIChatConversationMetricsResource.js'; import { AIChatFunctionsService } from './AIChatFunctionsService.js'; import { AIFunctionsResource } from './AIFunctionsResource.js'; -import { AIChatProfilesResource } from './AIChatProfilesResource.js'; export default ModuleRegistry.add({ name: '@cloudbeaver/plugin-ai-chat', @@ -49,7 +48,6 @@ export default ModuleRegistry.add({ .addSingleton(AIChatConversationScopeResource) .addSingleton(AIChatConversationMetricsResource) .addSingleton(AIChatFunctionsService) - .addSingleton(AIFunctionsResource) - .addSingleton(AIChatProfilesResource); + .addSingleton(AIFunctionsResource); }, }); diff --git a/webapp/packages/plugin-ai-user-profile/.gitignore b/webapp/packages/plugin-ai-user-profile/.gitignore new file mode 100644 index 00000000000..15bc16c7c31 --- /dev/null +++ b/webapp/packages/plugin-ai-user-profile/.gitignore @@ -0,0 +1,17 @@ +# dependencies +/node_modules + +# testing +/coverage + +# production +/lib + +# misc +.DS_Store +.env* + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/webapp/packages/plugin-ai-user-profile/package.json b/webapp/packages/plugin-ai-user-profile/package.json new file mode 100644 index 00000000000..ef88bf300ce --- /dev/null +++ b/webapp/packages/plugin-ai-user-profile/package.json @@ -0,0 +1,46 @@ +{ + "name": "@cloudbeaver/plugin-ai-user-profile", + "type": "module", + "sideEffects": [ + "./lib/module.js", + "./lib/index.js", + "src/**/*.css", + "public/**/*" + ], + "version": "0.1.0", + "description": "", + "license": "Apache-2.0", + "exports": { + ".": "./lib/index.js", + "./module": "./lib/module.js" + }, + "scripts": { + "build": "tsc -b", + "clean": "rimraf --glob lib", + "lint": "eslint ./src/ --ext .ts,.tsx", + "validate-dependencies": "core-cli-validate-dependencies" + }, + "dependencies": { + "@cloudbeaver/core-authentication": "workspace:*", + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-events": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "@cloudbeaver/core-resource": "workspace:*", + "@cloudbeaver/core-root": "workspace:*", + "@cloudbeaver/plugin-ai": "workspace:*", + "@cloudbeaver/plugin-user-profile": "workspace:*", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "tslib": "^2" + }, + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@types/react": "^19", + "rimraf": "^6", + "typescript": "^5" + } +} diff --git a/webapp/packages/plugin-ai-user-profile/src/AIUserProfileBootstrap.ts b/webapp/packages/plugin-ai-user-profile/src/AIUserProfileBootstrap.ts new file mode 100644 index 00000000000..777b5ca123a --- /dev/null +++ b/webapp/packages/plugin-ai-user-profile/src/AIUserProfileBootstrap.ts @@ -0,0 +1,52 @@ +/* + * 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 { AppAuthService } from '@cloudbeaver/core-authentication'; +import { importLazyComponent } from '@cloudbeaver/core-blocks'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { CachedMapAllKey, getCachedMapResourceLoaderState } from '@cloudbeaver/core-resource'; +import { FEATURE_AI_ID, ServerConfigResource } from '@cloudbeaver/core-root'; +import { UserAIProfileResource } from '@cloudbeaver/plugin-ai'; +import { UserProfileTabsService } from '@cloudbeaver/plugin-user-profile'; + +const AIProfilesPanel = importLazyComponent(() => import('./components/AIProfilesPanel.js').then(module => module.AIProfilesPanel)); + +const AI_PROFILES_TAB_ID = 'ai_profiles'; + +@injectable(() => [UserProfileTabsService, AppAuthService, ServerConfigResource, UserAIProfileResource]) +export class AIUserProfileBootstrap extends Bootstrap { + constructor( + private readonly userProfileTabsService: UserProfileTabsService, + private readonly appAuthService: AppAuthService, + private readonly serverConfigResource: ServerConfigResource, + private readonly userAIProfileResource: UserAIProfileResource, + ) { + super(); + } + + override register(): void { + this.userProfileTabsService.tabContainer.add({ + key: AI_PROFILES_TAB_ID, + name: 'plugin_ai_user_profile_tab_label', + order: 4, + getLoader: () => + getCachedMapResourceLoaderState(this.userAIProfileResource, () => + this.appAuthService.authenticated && this.serverConfigResource.isFeatureEnabled(FEATURE_AI_ID, true) ? CachedMapAllKey : null, + ), + isHidden: () => !this.isAvailable(), + panel: () => AIProfilesPanel, + }); + } + + private isAvailable(): boolean { + return ( + this.appAuthService.authenticated && + this.serverConfigResource.isFeatureEnabled(FEATURE_AI_ID, true) && + (!this.userAIProfileResource.isLoaded(CachedMapAllKey) || this.userAIProfileResource.values.length > 0) + ); + } +} diff --git a/webapp/packages/plugin-ai-user-profile/src/LocaleService.ts b/webapp/packages/plugin-ai-user-profile/src/LocaleService.ts new file mode 100644 index 00000000000..fc05f72073e --- /dev/null +++ b/webapp/packages/plugin-ai-user-profile/src/LocaleService.ts @@ -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 { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { LocalizationService } from '@cloudbeaver/core-localization'; + +@injectable(() => [LocalizationService]) +export class LocaleService extends Bootstrap { + constructor(private readonly localizationService: LocalizationService) { + super(); + } + + override register(): void { + this.localizationService.addProvider(this.provider.bind(this)); + } + + private async provider(locale: string) { + switch (locale) { + case 'ru': + return (await import('./locales/ru.js')).default; + case 'zh': + return (await import('./locales/zh.js')).default; + case 'fr': + return (await import('./locales/fr.js')).default; + default: + return (await import('./locales/en.js')).default; + } + } +} diff --git a/webapp/packages/plugin-ai-user-profile/src/components/AIProfilesPanel.tsx b/webapp/packages/plugin-ai-user-profile/src/components/AIProfilesPanel.tsx new file mode 100644 index 00000000000..aa48d985bdc --- /dev/null +++ b/webapp/packages/plugin-ai-user-profile/src/components/AIProfilesPanel.tsx @@ -0,0 +1,57 @@ +/* + * 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 { ColoredContainer, Container, Group, TextPlaceholder, ToolsAction, ToolsPanel, useResource, useTranslate } from '@cloudbeaver/core-blocks'; +import { CachedMapAllKey } from '@cloudbeaver/core-resource'; +import { useService } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { AiEnginesResource, UserAIProfileResource } from '@cloudbeaver/plugin-ai'; + +import { AIProfilesTable, type IAIProfile } from './AIProfilesTable.js'; + +export const AIProfilesPanel = observer(function AIProfilesPanel() { + const translate = useTranslate(); + const notificationService = useService(NotificationService); + const profilesLoader = useResource(AIProfilesPanel, UserAIProfileResource, CachedMapAllKey); + const enginesLoader = useResource(AIProfilesPanel, AiEnginesResource, undefined); + const profiles = profilesLoader.data.filter((profile): profile is IAIProfile => profile !== undefined); + + async function refresh(): Promise { + try { + await Promise.all([profilesLoader.reload(), enginesLoader.reload()]); + } catch (exception: any) { + notificationService.logException(exception, 'plugin_ai_user_profile_refresh_failed'); + } + } + + return ( + + + + + {translate('ui_refresh')} + + + + + {profiles.length ? ( + + ) : ( + {translate('plugin_ai_user_profile_empty')} + )} + + + ); +}); diff --git a/webapp/packages/plugin-ai-user-profile/src/components/AIProfilesTable.tsx b/webapp/packages/plugin-ai-user-profile/src/components/AIProfilesTable.tsx new file mode 100644 index 00000000000..6563cc2945b --- /dev/null +++ b/webapp/packages/plugin-ai-user-profile/src/components/AIProfilesTable.tsx @@ -0,0 +1,109 @@ +/* + * 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 { + Button, + IconOrImage, + Table, + TableBody, + TableColumnHeader, + TableColumnValue, + TableHeader, + TableItem, + useTranslate, +} from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { AIProfileCredentialsDialogService, type EngineInfo } from '@cloudbeaver/plugin-ai'; + +export interface IAIProfile { + id: string; + name: string; + engineId: string; + global: boolean; + credentialsSaved: boolean; +} + +interface Props { + profiles: IAIProfile[]; + engines: EngineInfo[]; +} + +export const AIProfilesTable = observer(function AIProfilesTable({ profiles, engines }) { + const translate = useTranslate(); + const credentialsDialogService = useService(AIProfileCredentialsDialogService); + const notificationService = useService(NotificationService); + + async function editCredentials(profileId: string): Promise { + try { + await credentialsDialogService.open(profileId); + } catch (exception: any) { + notificationService.logException(exception, 'plugin_ai_user_profile_credentials_edit_failed'); + } + } + + return ( + profile.id)}> + + {translate('plugin_ai_user_profile_column_profile')} + {translate('plugin_ai_user_profile_column_engine')} + {translate('plugin_ai_user_profile_column_credential_source')} + {translate('plugin_ai_user_profile_column_status')} + + + {profiles.map(profile => { + const engine = engines.find(engine => engine.id === profile.engineId); + const engineName = engine?.name ?? profile.engineId; + + return ( + + +
+ {profile.global && } + {profile.name} +
+
+ +
+ {engine?.icon && } + {engineName} +
+
+ + {translate( + profile.global ? 'plugin_ai_user_profile_credential_source_administrator' : 'plugin_ai_user_profile_credential_source_user', + )} + + + {profile.global ? ( + translate('plugin_ai_user_profile_status_managed') + ) : ( +
+ + {translate( + profile.credentialsSaved ? 'plugin_ai_user_profile_status_configured' : 'plugin_ai_user_profile_status_not_configured', + )} + + +
+ )} +
+
+ ); + })} +
+
+ ); +}); diff --git a/webapp/packages/plugin-ai-user-profile/src/index.ts b/webapp/packages/plugin-ai-user-profile/src/index.ts new file mode 100644 index 00000000000..36eb47b0b19 --- /dev/null +++ b/webapp/packages/plugin-ai-user-profile/src/index.ts @@ -0,0 +1,9 @@ +/* + * 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 './module.js'; diff --git a/webapp/packages/plugin-ai-user-profile/src/locales/en.ts b/webapp/packages/plugin-ai-user-profile/src/locales/en.ts new file mode 100644 index 00000000000..fe1d3f3f77c --- /dev/null +++ b/webapp/packages/plugin-ai-user-profile/src/locales/en.ts @@ -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. + */ +export default [ + ['plugin_ai_user_profile_tab_label', 'AI Profiles'], + ['plugin_ai_user_profile_refresh_tooltip', 'Refresh AI profiles'], + ['plugin_ai_user_profile_refresh_failed', 'Failed to refresh AI profiles'], + ['plugin_ai_user_profile_empty', 'No AI profiles are available'], + ['plugin_ai_user_profile_column_profile', 'Profile'], + ['plugin_ai_user_profile_column_engine', 'Engine'], + ['plugin_ai_user_profile_column_credential_source', 'Credential source'], + ['plugin_ai_user_profile_column_status', 'Status'], + ['plugin_ai_user_profile_credential_source_administrator', 'Administrator'], + ['plugin_ai_user_profile_credential_source_user', 'User'], + ['plugin_ai_user_profile_status_managed', 'Managed by administrator'], + ['plugin_ai_user_profile_status_configured', 'Configured'], + ['plugin_ai_user_profile_status_not_configured', 'Not configured'], + ['plugin_ai_user_profile_action_configure_credentials', 'Configure credentials'], + ['plugin_ai_user_profile_action_edit_credentials', 'Edit credentials'], + ['plugin_ai_user_profile_credentials_edit_failed', 'Failed to edit AI profile credentials'], +]; diff --git a/webapp/packages/plugin-ai-user-profile/src/locales/fr.ts b/webapp/packages/plugin-ai-user-profile/src/locales/fr.ts new file mode 100644 index 00000000000..3ee6da699d7 --- /dev/null +++ b/webapp/packages/plugin-ai-user-profile/src/locales/fr.ts @@ -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. + */ +export default [ + ['plugin_ai_user_profile_tab_label', 'Profils IA'], + ['plugin_ai_user_profile_refresh_tooltip', 'Actualiser les profils IA'], + ['plugin_ai_user_profile_refresh_failed', "Échec de l'actualisation des profils IA"], + ['plugin_ai_user_profile_empty', "Aucun profil IA n'est disponible"], + ['plugin_ai_user_profile_column_profile', 'Profil'], + ['plugin_ai_user_profile_column_engine', 'Moteur'], + ['plugin_ai_user_profile_column_credential_source', 'Source des identifiants'], + ['plugin_ai_user_profile_column_status', 'Statut'], + ['plugin_ai_user_profile_credential_source_administrator', 'Administrateur'], + ['plugin_ai_user_profile_credential_source_user', 'Utilisateur'], + ['plugin_ai_user_profile_status_managed', "Géré par l'administrateur"], + ['plugin_ai_user_profile_status_configured', 'Configuré'], + ['plugin_ai_user_profile_status_not_configured', 'Non configuré'], + ['plugin_ai_user_profile_action_configure_credentials', 'Configurer les identifiants'], + ['plugin_ai_user_profile_action_edit_credentials', 'Modifier les identifiants'], + ['plugin_ai_user_profile_credentials_edit_failed', 'Échec de la modification des identifiants du profil IA'], +]; diff --git a/webapp/packages/plugin-ai-user-profile/src/locales/ru.ts b/webapp/packages/plugin-ai-user-profile/src/locales/ru.ts new file mode 100644 index 00000000000..06bf028a1c1 --- /dev/null +++ b/webapp/packages/plugin-ai-user-profile/src/locales/ru.ts @@ -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. + */ +export default [ + ['plugin_ai_user_profile_tab_label', 'Профили ИИ'], + ['plugin_ai_user_profile_refresh_tooltip', 'Обновить профили ИИ'], + ['plugin_ai_user_profile_refresh_failed', 'Не удалось обновить профили ИИ'], + ['plugin_ai_user_profile_empty', 'Нет доступных профилей ИИ'], + ['plugin_ai_user_profile_column_profile', 'Профиль'], + ['plugin_ai_user_profile_column_engine', 'Движок'], + ['plugin_ai_user_profile_column_credential_source', 'Источник учетных данных'], + ['plugin_ai_user_profile_column_status', 'Статус'], + ['plugin_ai_user_profile_credential_source_administrator', 'Администратор'], + ['plugin_ai_user_profile_credential_source_user', 'Пользователь'], + ['plugin_ai_user_profile_status_managed', 'Управляется администратором'], + ['plugin_ai_user_profile_status_configured', 'Настроено'], + ['plugin_ai_user_profile_status_not_configured', 'Не настроено'], + ['plugin_ai_user_profile_action_configure_credentials', 'Настроить учетные данные'], + ['plugin_ai_user_profile_action_edit_credentials', 'Изменить учетные данные'], + ['plugin_ai_user_profile_credentials_edit_failed', 'Не удалось изменить учетные данные профиля ИИ'], +]; diff --git a/webapp/packages/plugin-ai-user-profile/src/locales/zh.ts b/webapp/packages/plugin-ai-user-profile/src/locales/zh.ts new file mode 100644 index 00000000000..761564e3785 --- /dev/null +++ b/webapp/packages/plugin-ai-user-profile/src/locales/zh.ts @@ -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. + */ +export default [ + ['plugin_ai_user_profile_tab_label', 'AI 配置文件'], + ['plugin_ai_user_profile_refresh_tooltip', '刷新 AI 配置文件'], + ['plugin_ai_user_profile_refresh_failed', '无法刷新 AI 配置文件'], + ['plugin_ai_user_profile_empty', '没有可用的 AI 配置文件'], + ['plugin_ai_user_profile_column_profile', '配置文件'], + ['plugin_ai_user_profile_column_engine', '引擎'], + ['plugin_ai_user_profile_column_credential_source', '凭据来源'], + ['plugin_ai_user_profile_column_status', '状态'], + ['plugin_ai_user_profile_credential_source_administrator', '管理员'], + ['plugin_ai_user_profile_credential_source_user', '用户'], + ['plugin_ai_user_profile_status_managed', '由管理员管理'], + ['plugin_ai_user_profile_status_configured', '已配置'], + ['plugin_ai_user_profile_status_not_configured', '未配置'], + ['plugin_ai_user_profile_action_configure_credentials', '配置凭据'], + ['plugin_ai_user_profile_action_edit_credentials', '编辑凭据'], + ['plugin_ai_user_profile_credentials_edit_failed', '无法编辑 AI 配置文件凭据'], +]; diff --git a/webapp/packages/plugin-ai-user-profile/src/module.ts b/webapp/packages/plugin-ai-user-profile/src/module.ts new file mode 100644 index 00000000000..bb6019284c9 --- /dev/null +++ b/webapp/packages/plugin-ai-user-profile/src/module.ts @@ -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 { Bootstrap, ModuleRegistry } from '@cloudbeaver/core-di'; + +import { AIUserProfileBootstrap } from './AIUserProfileBootstrap.js'; +import { LocaleService } from './LocaleService.js'; + +export default ModuleRegistry.add({ + name: '@cloudbeaver/plugin-ai-user-profile', + + configure: serviceCollection => { + serviceCollection + .addSingleton(Bootstrap, LocaleService) + .addSingleton(Bootstrap, AIUserProfileBootstrap); + }, +}); diff --git a/webapp/packages/plugin-ai-user-profile/tsconfig.json b/webapp/packages/plugin-ai-user-profile/tsconfig.json new file mode 100644 index 00000000000..038a9d1f009 --- /dev/null +++ b/webapp/packages/plugin-ai-user-profile/tsconfig.json @@ -0,0 +1,51 @@ +{ + "extends": "@cloudbeaver/tsconfig/tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true + }, + "references": [ + { + "path": "../core-authentication" + }, + { + "path": "../core-blocks" + }, + { + "path": "../core-cli" + }, + { + "path": "../core-di" + }, + { + "path": "../core-events" + }, + { + "path": "../core-localization" + }, + { + "path": "../core-resource" + }, + { + "path": "../core-root" + }, + { + "path": "../plugin-ai" + }, + { + "path": "../plugin-user-profile" + } + ], + "include": [ + "__custom_mocks__/**/*", + "src/**/*", + "src/**/*.json", + "src/**/*.css" + ], + "exclude": [ + "**/node_modules", + "lib/**/*" + ] +} diff --git a/webapp/packages/plugin-ai/package.json b/webapp/packages/plugin-ai/package.json index 7a2dbe5ea78..2a176451db0 100644 --- a/webapp/packages/plugin-ai/package.json +++ b/webapp/packages/plugin-ai/package.json @@ -21,7 +21,12 @@ "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { + "@cloudbeaver/core-authentication": "workspace:*", + "@cloudbeaver/core-blocks": "workspace:*", "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-dialogs": "workspace:*", + "@cloudbeaver/core-events": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", "@cloudbeaver/core-resource": "workspace:*", "@cloudbeaver/core-root": "workspace:*", "@cloudbeaver/core-sdk": "workspace:*", diff --git a/webapp/packages/plugin-ai/src/AIProfileCredentialsDialog.tsx b/webapp/packages/plugin-ai/src/AIProfileCredentialsDialog.tsx new file mode 100644 index 00000000000..3d472142704 --- /dev/null +++ b/webapp/packages/plugin-ai/src/AIProfileCredentialsDialog.tsx @@ -0,0 +1,146 @@ +/* + * 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 { observable } from 'mobx'; +import { observer } from 'mobx-react-lite'; + +import { + Button, + CommonDialogBody, + CommonDialogFooter, + CommonDialogHeader, + CommonDialogWrapper, + ConfirmationDialog, + Container, + Fill, + InputField, + SAVED_VALUE_INDICATOR, + useObservableRef, + useTranslate, +} from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { CommonDialogService, DialogueStateResult, type DialogComponent } from '@cloudbeaver/core-dialogs'; +import { NotificationService } from '@cloudbeaver/core-events'; + +import type { IAIProfileCredentialsDialogPayload } from './IAIProfileCredentialsDialogPayload.js'; +import { UserAIProfileResource } from './UserAIProfileResource.js'; + +interface CredentialsDialogState { + token: string; + processing: boolean; + credentialsSaved: boolean; +} + +// TODO: Move this UI to a dedicated shared AI credentials UI package when package boundaries warrant it. +export const AIProfileCredentialsDialog: DialogComponent = observer(function AIProfileCredentialsDialog({ + payload, + resolveDialog, + rejectDialog, +}) { + const translate = useTranslate(); + const commonDialogService = useService(CommonDialogService); + const notificationService = useService(NotificationService); + const userAIProfileResource = useService(UserAIProfileResource); + const state = useObservableRef( + () => ({ token: '', processing: false, credentialsSaved: payload.credentialsSaved }), + { token: observable.ref, processing: observable.ref, credentialsSaved: observable.ref }, + false, + ); + + async function save(): Promise { + try { + state.processing = true; + if (state.token) { + const saved = await userAIProfileResource.saveCredentials(payload.profileId, state.token); + if (!saved) { + throw new Error(translate('plugin_ai_credentials_save_failed')); + } + state.token = ''; + } else if (!state.credentialsSaved) { + return; + } + + resolveDialog(); + } catch (exception: any) { + notificationService.logException(exception, 'plugin_ai_credentials_save_failed'); + } finally { + state.processing = false; + } + } + + async function resetCredentials(): Promise { + const { status } = await commonDialogService.open(ConfirmationDialog, { + title: translate('plugin_ai_credentials_reset_title'), + message: 'plugin_ai_credentials_reset_confirmation', + confirmActionText: 'plugin_ai_credentials_reset', + }); + + if (status === DialogueStateResult.Resolved) { + try { + state.processing = true; + const reset = await userAIProfileResource.resetCredentials(payload.profileId); + if (!reset) { + throw new Error(translate('plugin_ai_credentials_reset_failed')); + } + state.token = ''; + state.credentialsSaved = false; + } catch (exception: any) { + notificationService.logException(exception, 'plugin_ai_credentials_reset_failed'); + } finally { + state.processing = false; + } + } + } + + return ( + + + + + + {translate('plugin_ai_credentials_profile')} + + + {translate('plugin_ai_credentials_engine')} + + + {translate('plugin_ai_credentials_token')} + + + + + {state.credentialsSaved && ( + + )} + + + + + + ); +}); diff --git a/webapp/packages/plugin-ai/src/AIProfileCredentialsDialogLazy.ts b/webapp/packages/plugin-ai/src/AIProfileCredentialsDialogLazy.ts new file mode 100644 index 00000000000..1de22b5974d --- /dev/null +++ b/webapp/packages/plugin-ai/src/AIProfileCredentialsDialogLazy.ts @@ -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 AIProfileCredentialsDialog = importLazyComponent(() => + import('./AIProfileCredentialsDialog.js').then(module => module.AIProfileCredentialsDialog), +); diff --git a/webapp/packages/plugin-ai/src/AIProfileCredentialsService.ts b/webapp/packages/plugin-ai/src/AIProfileCredentialsService.ts new file mode 100644 index 00000000000..27d2da88d37 --- /dev/null +++ b/webapp/packages/plugin-ai/src/AIProfileCredentialsService.ts @@ -0,0 +1,45 @@ +/* + * 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 { injectable } from '@cloudbeaver/core-di'; +import { CommonDialogService, DialogueStateResult, type DialogResult } from '@cloudbeaver/core-dialogs'; +import { NotificationService } from '@cloudbeaver/core-events'; + +import { AiEnginesResource } from './AiEnginesResource.js'; +import { AIProfileCredentialsDialog } from './AIProfileCredentialsDialogLazy.js'; +import { UserAIProfileResource } from './UserAIProfileResource.js'; + +@injectable(() => [CommonDialogService, NotificationService, UserAIProfileResource, AiEnginesResource]) +export class AIProfileCredentialsService { + constructor( + private readonly commonDialogService: CommonDialogService, + private readonly notificationService: NotificationService, + private readonly userAIProfileResource: UserAIProfileResource, + private readonly aiEnginesResource: AiEnginesResource, + ) {} + + async open(profileId: string): Promise> { + const [profile] = await Promise.all([this.userAIProfileResource.load(profileId), this.aiEnginesResource.load()]); + + if (!profile) { + this.notificationService.logError({ title: 'plugin_ai_credentials_profile_not_found' }); + return { status: DialogueStateResult.Rejected }; + } + + const engine = this.aiEnginesResource.data.find(engine => engine.id === profile.engineId); + return this.commonDialogService.open(AIProfileCredentialsDialog, { + profileId: profile.id, + profileName: profile.name, + engineName: engine?.name ?? profile.engineId, + engineIcon: engine?.icon, + credentialsSaved: profile.credentialsSaved, + }); + } +} + +export { AIProfileCredentialsService as AIProfileCredentialsDialogService }; diff --git a/webapp/packages/plugin-ai/src/AIProfileCredentialsUtils.ts b/webapp/packages/plugin-ai/src/AIProfileCredentialsUtils.ts new file mode 100644 index 00000000000..01f3248e2b5 --- /dev/null +++ b/webapp/packages/plugin-ai/src/AIProfileCredentialsUtils.ts @@ -0,0 +1,21 @@ +/* + * 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 { AIProfile } from './UserAIProfileResource.js'; + +export function supportsUserCredentials(properties: ReadonlyArray<{ id?: string; features: readonly string[] }>): boolean { + return properties.some(property => property.id === 'token' && property.features.includes('password')); +} + +export function requireGlobalProfileToken(properties: readonly T[], global: boolean): T[] { + return properties.map(property => (global && property.id === 'token' ? { ...property, required: true } : property)); +} + +export function requiresUserCredentials(profile: Pick): boolean { + return !profile.global && !profile.credentialsSaved; +} diff --git a/webapp/packages/plugin-ai-chat/src/AIChatProfilesResource.ts b/webapp/packages/plugin-ai/src/AISettingsResource.ts similarity index 57% rename from webapp/packages/plugin-ai-chat/src/AIChatProfilesResource.ts rename to webapp/packages/plugin-ai/src/AISettingsResource.ts index 5e5e0a111eb..f177d16c8e5 100644 --- a/webapp/packages/plugin-ai-chat/src/AIChatProfilesResource.ts +++ b/webapp/packages/plugin-ai/src/AISettingsResource.ts @@ -9,33 +9,25 @@ import { injectable } from '@cloudbeaver/core-di'; import { CachedDataResource } from '@cloudbeaver/core-resource'; import { ServerConfigResource, ServerEventId, WorkspaceConfigEventHandler } from '@cloudbeaver/core-root'; -import { type AiConfigurationProfileInfo, GraphQLService } from '@cloudbeaver/core-sdk'; +import { type AiSettingsInfo, GraphQLService } from '@cloudbeaver/core-sdk'; -export type AIChatProfile = AiConfigurationProfileInfo; +export type AISettings = AiSettingsInfo; @injectable(() => [GraphQLService, ServerConfigResource, WorkspaceConfigEventHandler]) -export class AIChatProfilesResource extends CachedDataResource { +export class AISettingsResource extends CachedDataResource { constructor( private readonly graphQLService: GraphQLService, serverConfigResource: ServerConfigResource, workspaceConfigEventHandler: WorkspaceConfigEventHandler, ) { - super(() => []); + super(() => null); this.sync(serverConfigResource); - - workspaceConfigEventHandler.onEvent( - ServerEventId.CbWorkspaceConfigChanged, - () => { - this.markOutdated(); - }, - undefined, - this, - ); + workspaceConfigEventHandler.onEvent(ServerEventId.CbWorkspaceConfigChanged, () => this.markOutdated(), undefined, this); } - protected async loader(): Promise { - const { profiles } = await this.graphQLService.sdk.getAiProfiles(); - return profiles; + protected async loader(): Promise { + const { settings } = await this.graphQLService.sdk.getAiSettings(); + return settings; } } diff --git a/webapp/packages/plugin-ai/src/IAIProfileCredentialsDialogPayload.ts b/webapp/packages/plugin-ai/src/IAIProfileCredentialsDialogPayload.ts new file mode 100644 index 00000000000..4fd6ac0a75f --- /dev/null +++ b/webapp/packages/plugin-ai/src/IAIProfileCredentialsDialogPayload.ts @@ -0,0 +1,15 @@ +/* + * 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 interface IAIProfileCredentialsDialogPayload { + profileId: string; + profileName: string; + engineName: string; + engineIcon?: string; + credentialsSaved: boolean; +} diff --git a/webapp/packages/plugin-ai/src/LocaleService.ts b/webapp/packages/plugin-ai/src/LocaleService.ts new file mode 100644 index 00000000000..6d42886d2be --- /dev/null +++ b/webapp/packages/plugin-ai/src/LocaleService.ts @@ -0,0 +1,34 @@ +/* + * 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 { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { LocalizationService } from '@cloudbeaver/core-localization'; + +@injectable(() => [LocalizationService]) +export class LocaleService extends Bootstrap { + constructor(private readonly localizationService: LocalizationService) { + super(); + } + + override register(): void { + this.localizationService.addProvider(this.provider.bind(this)); + } + + private async provider(locale: string) { + switch (locale) { + case 'ru': + return (await import('./locales/ru.js')).default; + case 'zh': + return (await import('./locales/zh.js')).default; + case 'fr': + return (await import('./locales/fr.js')).default; + default: + return (await import('./locales/en.js')).default; + } + } +} diff --git a/webapp/packages/plugin-ai/src/UserAIProfileResource.ts b/webapp/packages/plugin-ai/src/UserAIProfileResource.ts new file mode 100644 index 00000000000..da6829d20cf --- /dev/null +++ b/webapp/packages/plugin-ai/src/UserAIProfileResource.ts @@ -0,0 +1,95 @@ +/* + * 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 { UserInfoResource } from '@cloudbeaver/core-authentication'; +import { injectable } from '@cloudbeaver/core-di'; +import { CachedMapAllKey, CachedMapResource, resourceKeyList } from '@cloudbeaver/core-resource'; +import { ServerConfigResource, ServerEventId, WorkspaceConfigEventHandler } from '@cloudbeaver/core-root'; +import { type AiConfigurationProfileInfo, GraphQLService } from '@cloudbeaver/core-sdk'; + +export type AIProfile = AiConfigurationProfileInfo; + +@injectable(() => [GraphQLService, ServerConfigResource, WorkspaceConfigEventHandler, UserInfoResource]) +export class UserAIProfileResource extends CachedMapResource { + constructor( + private readonly graphQLService: GraphQLService, + serverConfigResource: ServerConfigResource, + workspaceConfigEventHandler: WorkspaceConfigEventHandler, + userInfoResource: UserInfoResource, + ) { + super(); + + this.sync( + serverConfigResource, + () => undefined, + () => CachedMapAllKey, + ); + + workspaceConfigEventHandler.onEvent(ServerEventId.CbWorkspaceConfigChanged, () => this.markOutdated(CachedMapAllKey), undefined, this); + userInfoResource.onUserChange.addHandler(() => this.markOutdated(CachedMapAllKey)); + } + + setProfile(profile: Omit & Partial>): void { + this.set(profile.id, { + ...profile, + credentialsSaved: profile.credentialsSaved ?? this.get(profile.id)?.credentialsSaved ?? false, + }); + } + + removeProfile(profileId: string): void { + this.delete(profileId); + } + + setCredentialsSaved(profileId: string, credentialsSaved: boolean): void { + const profile = this.get(profileId); + if (profile) { + this.set(profileId, { ...profile, credentialsSaved }); + } + } + + saveCredentials(profileId: string, token: string): Promise { + if (!token) { + return Promise.resolve(false); + } + + const profile = this.get(profileId); + if (!profile || profile.global) { + return Promise.resolve(false); + } + return this.updateCredentials(profileId, token, true); + } + + resetCredentials(profileId: string): Promise { + const profile = this.get(profileId); + if (!profile || profile.global) { + return Promise.resolve(false); + } + return this.updateCredentials(profileId, '', false); + } + + protected async loader(): Promise> { + const { profiles } = await this.graphQLService.sdk.getAiProfiles(); + this.replace(resourceKeyList(profiles.map(profile => profile.id)), profiles); + return this.data; + } + + protected validateKey(key: string): boolean { + return typeof key === 'string'; + } + + private async updateCredentials(profileId: string, token: string, credentialsSaved: boolean): Promise { + const { result } = await this.graphQLService.sdk.saveAiProfileCredentials({ + profileId, + credentials: { properties: { token } }, + }); + if (result) { + this.setCredentialsSaved(profileId, credentialsSaved); + } + return result; + } +} diff --git a/webapp/packages/plugin-ai/src/index.ts b/webapp/packages/plugin-ai/src/index.ts index f8bf66e1d57..dc5c5d438ab 100644 --- a/webapp/packages/plugin-ai/src/index.ts +++ b/webapp/packages/plugin-ai/src/index.ts @@ -9,3 +9,9 @@ import './module.js'; export * from './AiEnginesResource.js'; +export * from './AIProfileCredentialsDialogLazy.js'; +export * from './AIProfileCredentialsService.js'; +export * from './AIProfileCredentialsUtils.js'; +export * from './UserAIProfileResource.js'; +export * from './AISettingsResource.js'; +export * from './IAIProfileCredentialsDialogPayload.js'; diff --git a/webapp/packages/plugin-ai/src/locales/en.ts b/webapp/packages/plugin-ai/src/locales/en.ts new file mode 100644 index 00000000000..1f9e6f455c0 --- /dev/null +++ b/webapp/packages/plugin-ai/src/locales/en.ts @@ -0,0 +1,21 @@ +/* + * 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 default [ + ['plugin_ai_credentials_dialog_title', 'AI profile credentials'], + ['plugin_ai_credentials_dialog_description', 'Provide the credentials used by this AI profile for your user account.'], + ['plugin_ai_credentials_profile', 'Profile'], + ['plugin_ai_credentials_engine', 'Engine'], + ['plugin_ai_credentials_token', 'API Token'], + ['plugin_ai_credentials_reset', 'Reset Credentials'], + ['plugin_ai_credentials_reset_title', 'Reset AI profile credentials'], + ['plugin_ai_credentials_reset_confirmation', 'Are you sure you want to reset the saved credentials?'], + ['plugin_ai_credentials_profile_not_found', 'AI profile not found'], + ['plugin_ai_credentials_save_failed', 'Failed to save AI profile credentials'], + ['plugin_ai_credentials_reset_failed', 'Failed to reset AI profile credentials'], +]; diff --git a/webapp/packages/plugin-ai/src/locales/fr.ts b/webapp/packages/plugin-ai/src/locales/fr.ts new file mode 100644 index 00000000000..ae6cd83f8b1 --- /dev/null +++ b/webapp/packages/plugin-ai/src/locales/fr.ts @@ -0,0 +1,21 @@ +/* + * 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 default [ + ['plugin_ai_credentials_dialog_title', 'Identifiants du profil IA'], + ['plugin_ai_credentials_dialog_description', 'Fournissez les identifiants utilisés par ce profil IA pour votre compte utilisateur.'], + ['plugin_ai_credentials_profile', 'Profil'], + ['plugin_ai_credentials_engine', 'Moteur'], + ['plugin_ai_credentials_token', 'Jeton API'], + ['plugin_ai_credentials_reset', 'Réinitialiser les identifiants'], + ['plugin_ai_credentials_reset_title', 'Réinitialiser les identifiants du profil IA'], + ['plugin_ai_credentials_reset_confirmation', 'Voulez-vous vraiment réinitialiser les identifiants enregistrés ?'], + ['plugin_ai_credentials_profile_not_found', 'Profil IA introuvable'], + ['plugin_ai_credentials_save_failed', "Échec de l'enregistrement des identifiants du profil IA"], + ['plugin_ai_credentials_reset_failed', 'Échec de la réinitialisation des identifiants du profil IA'], +]; diff --git a/webapp/packages/plugin-ai/src/locales/ru.ts b/webapp/packages/plugin-ai/src/locales/ru.ts new file mode 100644 index 00000000000..70138fe46dd --- /dev/null +++ b/webapp/packages/plugin-ai/src/locales/ru.ts @@ -0,0 +1,21 @@ +/* + * 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 default [ + ['plugin_ai_credentials_dialog_title', 'Учетные данные профиля ИИ'], + ['plugin_ai_credentials_dialog_description', 'Укажите учетные данные, которые этот профиль ИИ будет использовать для вашей учетной записи.'], + ['plugin_ai_credentials_profile', 'Профиль'], + ['plugin_ai_credentials_engine', 'Движок'], + ['plugin_ai_credentials_token', 'API-токен'], + ['plugin_ai_credentials_reset', 'Сбросить учетные данные'], + ['plugin_ai_credentials_reset_title', 'Сброс учетных данных профиля ИИ'], + ['plugin_ai_credentials_reset_confirmation', 'Вы уверены, что хотите сбросить сохраненные учетные данные?'], + ['plugin_ai_credentials_profile_not_found', 'Профиль ИИ не найден'], + ['plugin_ai_credentials_save_failed', 'Не удалось сохранить учетные данные профиля ИИ'], + ['plugin_ai_credentials_reset_failed', 'Не удалось сбросить учетные данные профиля ИИ'], +]; diff --git a/webapp/packages/plugin-ai/src/locales/zh.ts b/webapp/packages/plugin-ai/src/locales/zh.ts new file mode 100644 index 00000000000..0d28d17d7ac --- /dev/null +++ b/webapp/packages/plugin-ai/src/locales/zh.ts @@ -0,0 +1,21 @@ +/* + * 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 default [ + ['plugin_ai_credentials_dialog_title', 'AI 配置文件凭据'], + ['plugin_ai_credentials_dialog_description', '提供此 AI 配置文件用于您的用户账户的凭据。'], + ['plugin_ai_credentials_profile', '配置文件'], + ['plugin_ai_credentials_engine', '引擎'], + ['plugin_ai_credentials_token', 'API 令牌'], + ['plugin_ai_credentials_reset', '重置凭据'], + ['plugin_ai_credentials_reset_title', '重置 AI 配置文件凭据'], + ['plugin_ai_credentials_reset_confirmation', '确定要重置已保存的凭据吗?'], + ['plugin_ai_credentials_profile_not_found', '未找到 AI 配置文件'], + ['plugin_ai_credentials_save_failed', '无法保存 AI 配置文件凭据'], + ['plugin_ai_credentials_reset_failed', '无法重置 AI 配置文件凭据'], +]; diff --git a/webapp/packages/plugin-ai/src/module.ts b/webapp/packages/plugin-ai/src/module.ts index 49a6eb67fbf..54948fb2ff0 100644 --- a/webapp/packages/plugin-ai/src/module.ts +++ b/webapp/packages/plugin-ai/src/module.ts @@ -6,14 +6,26 @@ * you may not use this file except in compliance with the License. */ -import { Dependency, ModuleRegistry, proxy } from '@cloudbeaver/core-di'; +import { Bootstrap, Dependency, ModuleRegistry, proxy } from '@cloudbeaver/core-di'; import { AiEnginesResource } from './AiEnginesResource.js'; +import { AIProfileCredentialsService } from './AIProfileCredentialsService.js'; +import { UserAIProfileResource } from './UserAIProfileResource.js'; +import { AISettingsResource } from './AISettingsResource.js'; +import { LocaleService } from './LocaleService.js'; export default ModuleRegistry.add({ name: '@cloudbeaver/plugin-ai', configure: serviceCollection => { - serviceCollection.addSingleton(Dependency, proxy(AiEnginesResource)).addSingleton(AiEnginesResource); + serviceCollection + .addSingleton(Bootstrap, LocaleService) + .addSingleton(Dependency, proxy(AiEnginesResource)) + .addSingleton(Dependency, proxy(UserAIProfileResource)) + .addSingleton(Dependency, proxy(AISettingsResource)) + .addSingleton(AiEnginesResource) + .addSingleton(UserAIProfileResource) + .addSingleton(AISettingsResource) + .addSingleton(AIProfileCredentialsService); }, }); diff --git a/webapp/packages/plugin-ai/tsconfig.json b/webapp/packages/plugin-ai/tsconfig.json index a68c576f611..f41ca33ed46 100644 --- a/webapp/packages/plugin-ai/tsconfig.json +++ b/webapp/packages/plugin-ai/tsconfig.json @@ -11,12 +11,27 @@ "**/node_modules" ], "references": [ + { + "path": "../core-authentication" + }, + { + "path": "../core-blocks" + }, { "path": "../core-cli" }, { "path": "../core-di" }, + { + "path": "../core-dialogs" + }, + { + "path": "../core-events" + }, + { + "path": "../core-localization" + }, { "path": "../core-resource" }, diff --git a/webapp/packages/plugin-set-common/package.json b/webapp/packages/plugin-set-common/package.json index 86ab6c29712..d0e739007ee 100644 --- a/webapp/packages/plugin-set-common/package.json +++ b/webapp/packages/plugin-set-common/package.json @@ -59,6 +59,7 @@ "@cloudbeaver/plugin-ai": "workspace:*", "@cloudbeaver/plugin-ai-administration": "workspace:*", "@cloudbeaver/plugin-ai-chat": "workspace:*", + "@cloudbeaver/plugin-ai-user-profile": "workspace:*", "@cloudbeaver/plugin-app-logo": "workspace:*", "@cloudbeaver/plugin-app-logo-administration": "workspace:*", "@cloudbeaver/plugin-async-task-confirmation": "workspace:^", diff --git a/webapp/packages/plugin-set-common/src/index.ts b/webapp/packages/plugin-set-common/src/index.ts index ec3d44333a1..3c6a938fef0 100644 --- a/webapp/packages/plugin-set-common/src/index.ts +++ b/webapp/packages/plugin-set-common/src/index.ts @@ -121,6 +121,7 @@ import pluginDataViewerReferences from '@cloudbeaver/plugin-data-viewer-referenc import pluginAiChat from '@cloudbeaver/plugin-ai-chat/module'; import pluginAi from '@cloudbeaver/plugin-ai/module'; import pluginAiAdministration from '@cloudbeaver/plugin-ai-administration/module'; +import pluginAiUserProfile from '@cloudbeaver/plugin-ai-user-profile/module'; import pluginConnectionFormAi from '@cloudbeaver/plugin-connection-form-ai/module'; const core = [ @@ -242,5 +243,6 @@ export const commonSet = [ pluginAiChat, pluginAi, pluginAiAdministration, + pluginAiUserProfile, pluginConnectionFormAi, ]; diff --git a/webapp/packages/plugin-set-common/tsconfig.json b/webapp/packages/plugin-set-common/tsconfig.json index 9027a9d8c12..cd5e8fbf4f3 100644 --- a/webapp/packages/plugin-set-common/tsconfig.json +++ b/webapp/packages/plugin-set-common/tsconfig.json @@ -124,6 +124,9 @@ { "path": "../plugin-ai-chat" }, + { + "path": "../plugin-ai-user-profile" + }, { "path": "../plugin-app-logo" }, diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 5e51f61a229..2d6b25783dd 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -2546,12 +2546,43 @@ __metadata: languageName: unknown linkType: soft +"@cloudbeaver/plugin-ai-user-profile@workspace:*, @cloudbeaver/plugin-ai-user-profile@workspace:packages/plugin-ai-user-profile": + version: 0.0.0-use.local + resolution: "@cloudbeaver/plugin-ai-user-profile@workspace:packages/plugin-ai-user-profile" + dependencies: + "@cloudbeaver/core-authentication": "workspace:*" + "@cloudbeaver/core-blocks": "workspace:*" + "@cloudbeaver/core-cli": "workspace:*" + "@cloudbeaver/core-di": "workspace:*" + "@cloudbeaver/core-events": "workspace:*" + "@cloudbeaver/core-localization": "workspace:*" + "@cloudbeaver/core-resource": "workspace:*" + "@cloudbeaver/core-root": "workspace:*" + "@cloudbeaver/plugin-ai": "workspace:*" + "@cloudbeaver/plugin-user-profile": "workspace:*" + "@cloudbeaver/tsconfig": "workspace:*" + "@types/react": "npm:^19" + mobx: "npm:^6" + mobx-react-lite: "npm:^4" + react: "npm:^19" + react-dom: "npm:^19" + rimraf: "npm:^6" + tslib: "npm:^2" + typescript: "npm:^5" + languageName: unknown + linkType: soft + "@cloudbeaver/plugin-ai@workspace:*, @cloudbeaver/plugin-ai@workspace:packages/plugin-ai": version: 0.0.0-use.local resolution: "@cloudbeaver/plugin-ai@workspace:packages/plugin-ai" dependencies: + "@cloudbeaver/core-authentication": "workspace:*" + "@cloudbeaver/core-blocks": "workspace:*" "@cloudbeaver/core-cli": "workspace:*" "@cloudbeaver/core-di": "workspace:*" + "@cloudbeaver/core-dialogs": "workspace:*" + "@cloudbeaver/core-events": "workspace:*" + "@cloudbeaver/core-localization": "workspace:*" "@cloudbeaver/core-resource": "workspace:*" "@cloudbeaver/core-root": "workspace:*" "@cloudbeaver/core-sdk": "workspace:*" @@ -4280,6 +4311,7 @@ __metadata: "@cloudbeaver/plugin-ai": "workspace:*" "@cloudbeaver/plugin-ai-administration": "workspace:*" "@cloudbeaver/plugin-ai-chat": "workspace:*" + "@cloudbeaver/plugin-ai-user-profile": "workspace:*" "@cloudbeaver/plugin-app-logo": "workspace:*" "@cloudbeaver/plugin-app-logo-administration": "workspace:*" "@cloudbeaver/plugin-async-task-confirmation": "workspace:^" From 876de8d97dddf5897ae05c92e87bccd7d34f3e61 Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Wed, 2 Sep 2026 21:39:17 +0200 Subject: [PATCH 03/31] dbeaver/pro#9532 splits ai profiles feature to modules respectively --- .../plugin-ai-administration/package.json | 7 +- .../src/AIAdministrationPage.tsx | 4 +- .../src/AIAdministrationProfilesTabPanel.tsx | 23 ----- .../src/AIAdministrationTabsService.ts | 13 +-- .../src/AIProfiles/AIProfilesResource.ts | 91 ------------------- .../AdministrationAISettingsInfoPart.ts | 6 +- ...getAdministrationAISettingsFormInfoPart.ts | 6 +- .../src/AISettingsResource.ts | 53 ----------- .../src/AISettingsService.ts | 13 ++- .../plugin-ai-administration/src/index.ts | 1 + .../src/locales/de.ts | 40 +------- .../src/locales/en.ts | 40 +------- .../src/locales/fr.ts | 40 +------- .../src/locales/ru.ts | 40 +------- .../plugin-ai-administration/src/module.ts | 15 +-- .../plugin-ai-administration/tsconfig.json | 8 +- webapp/packages/plugin-ai-chat/package.json | 1 + .../AIChatConversationProfile.tsx | 11 ++- .../AIChatConversationScope.tsx | 4 +- .../AIChatConversationsResource.ts | 9 +- .../AIChatMessage/AIChatMessageService.ts | 19 ++-- webapp/packages/plugin-ai-chat/tsconfig.json | 3 + .../.gitignore | 1 + .../package.json | 57 ++++++++++++ .../AIProfiles/AIEnginePropertiesResource.ts | 0 .../AIProfileForm/AIProfileForm.tsx | 0 .../AIProfileForm/AIProfileFormPanel.tsx | 0 .../AIProfileForm/AIProfileFormService.ts | 0 .../AIProfileFormTabBootstrap.ts | 0 .../AIProfileForm/IAIProfileFormProps.ts | 0 .../AIProfileForm/IAIProfileFormState.ts | 0 .../Options/AIProfileFormPart.ts | 16 ++-- .../Options/AIProfileOptions.tsx | 19 ++-- .../Options/AIProfilePropertiesForm.tsx | 0 .../AIProfileForm/Options/AIProfileSchema.ts | 0 .../Options/getAIProfileFormPart.ts | 15 ++- .../AIProfilesAdministrationService.ts | 55 +++++++++++ .../src/AIProfiles/AIProfilesPanel.tsx | 33 +++---- .../src/AIProfiles/AIProfilesTable.tsx | 15 +-- .../AIProfilesToolsPanel.module.css | 0 .../src/AIProfiles/useAIProfilesTable.ts | 13 ++- .../utils/getObjectPropertiesValues.ts | 0 .../src/AIProfiles/utils/prepareProperties.ts | 0 .../src/AIProfilesAdministrationBootstrap.ts | 33 +++++++ .../src/AIProfilesTabPanel.tsx | 14 +++ .../src/LocaleService.ts | 34 +++++++ .../src/index.ts | 11 +++ .../src/locales/de.ts | 40 ++++++++ .../src/locales/en.ts | 40 ++++++++ .../src/locales/fr.ts | 40 ++++++++ .../src/locales/ru.ts | 40 ++++++++ .../src/module.ts | 32 +++++++ .../tsconfig.json | 81 +++++++++++++++++ webapp/packages/plugin-ai-profiles/.gitignore | 1 + .../packages/plugin-ai-profiles/package.json | 47 ++++++++++ .../src/AIProfileCredentialsDialog.tsx | 9 +- .../src/AIProfileCredentialsDialogLazy.ts | 0 .../src/AIProfileCredentialsService.ts | 21 +++-- .../src/AIProfileCredentialsUtils.ts | 6 +- .../src/AIProfilesResource.ts} | 2 +- .../src/IAIProfileCredentialsDialogPayload.ts | 0 .../src/LocaleService.ts | 0 .../packages/plugin-ai-profiles/src/index.ts | 14 +++ .../src/locales/en.ts | 0 .../src/locales/fr.ts | 0 .../src/locales/ru.ts | 0 .../src/locales/zh.ts | 0 .../packages/plugin-ai-profiles/src/module.ts | 25 +++++ .../packages/plugin-ai-profiles/tsconfig.json | 54 +++++++++++ .../plugin-ai-user-profile/package.json | 1 + .../src/AIUserProfileBootstrap.ts | 10 +- .../src/components/AIProfilesPanel.tsx | 5 +- .../src/components/AIProfilesTable.tsx | 7 +- .../plugin-ai-user-profile/tsconfig.json | 3 + webapp/packages/plugin-ai/package.json | 10 -- .../plugin-ai/src/AISettingsResource.ts | 9 +- webapp/packages/plugin-ai/src/index.ts | 5 - webapp/packages/plugin-ai/src/module.ts | 11 +-- webapp/packages/plugin-ai/tsconfig.json | 15 --- .../packages/plugin-set-common/package.json | 2 + .../packages/plugin-set-common/src/index.ts | 8 +- .../packages/plugin-set-common/tsconfig.json | 6 ++ webapp/yarn.lock | 75 +++++++++++++-- 83 files changed, 867 insertions(+), 515 deletions(-) delete mode 100644 webapp/packages/plugin-ai-administration/src/AIAdministrationProfilesTabPanel.tsx delete mode 100644 webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesResource.ts delete mode 100644 webapp/packages/plugin-ai-administration/src/AISettingsResource.ts create mode 100644 webapp/packages/plugin-ai-profiles-administration/.gitignore create mode 100644 webapp/packages/plugin-ai-profiles-administration/package.json rename webapp/packages/{plugin-ai-administration => plugin-ai-profiles-administration}/src/AIProfiles/AIEnginePropertiesResource.ts (100%) rename webapp/packages/{plugin-ai-administration => plugin-ai-profiles-administration}/src/AIProfiles/AIProfileForm/AIProfileForm.tsx (100%) rename webapp/packages/{plugin-ai-administration => plugin-ai-profiles-administration}/src/AIProfiles/AIProfileForm/AIProfileFormPanel.tsx (100%) rename webapp/packages/{plugin-ai-administration => plugin-ai-profiles-administration}/src/AIProfiles/AIProfileForm/AIProfileFormService.ts (100%) rename webapp/packages/{plugin-ai-administration => plugin-ai-profiles-administration}/src/AIProfiles/AIProfileForm/AIProfileFormTabBootstrap.ts (100%) rename webapp/packages/{plugin-ai-administration => plugin-ai-profiles-administration}/src/AIProfiles/AIProfileForm/IAIProfileFormProps.ts (100%) rename webapp/packages/{plugin-ai-administration => plugin-ai-profiles-administration}/src/AIProfiles/AIProfileForm/IAIProfileFormState.ts (100%) rename webapp/packages/{plugin-ai-administration => plugin-ai-profiles-administration}/src/AIProfiles/AIProfileForm/Options/AIProfileFormPart.ts (88%) rename webapp/packages/{plugin-ai-administration => plugin-ai-profiles-administration}/src/AIProfiles/AIProfileForm/Options/AIProfileOptions.tsx (91%) rename webapp/packages/{plugin-ai-administration => plugin-ai-profiles-administration}/src/AIProfiles/AIProfileForm/Options/AIProfilePropertiesForm.tsx (100%) rename webapp/packages/{plugin-ai-administration => plugin-ai-profiles-administration}/src/AIProfiles/AIProfileForm/Options/AIProfileSchema.ts (100%) rename webapp/packages/{plugin-ai-administration => plugin-ai-profiles-administration}/src/AIProfiles/AIProfileForm/Options/getAIProfileFormPart.ts (61%) create mode 100644 webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesAdministrationService.ts rename webapp/packages/{plugin-ai-administration => plugin-ai-profiles-administration}/src/AIProfiles/AIProfilesPanel.tsx (74%) rename webapp/packages/{plugin-ai-administration => plugin-ai-profiles-administration}/src/AIProfiles/AIProfilesTable.tsx (92%) rename webapp/packages/{plugin-ai-administration => plugin-ai-profiles-administration}/src/AIProfiles/AIProfilesToolsPanel.module.css (100%) rename webapp/packages/{plugin-ai-administration => plugin-ai-profiles-administration}/src/AIProfiles/useAIProfilesTable.ts (86%) rename webapp/packages/{plugin-ai-administration => plugin-ai-profiles-administration}/src/AIProfiles/utils/getObjectPropertiesValues.ts (100%) rename webapp/packages/{plugin-ai-administration => plugin-ai-profiles-administration}/src/AIProfiles/utils/prepareProperties.ts (100%) create mode 100644 webapp/packages/plugin-ai-profiles-administration/src/AIProfilesAdministrationBootstrap.ts create mode 100644 webapp/packages/plugin-ai-profiles-administration/src/AIProfilesTabPanel.tsx create mode 100644 webapp/packages/plugin-ai-profiles-administration/src/LocaleService.ts create mode 100644 webapp/packages/plugin-ai-profiles-administration/src/index.ts create mode 100644 webapp/packages/plugin-ai-profiles-administration/src/locales/de.ts create mode 100644 webapp/packages/plugin-ai-profiles-administration/src/locales/en.ts create mode 100644 webapp/packages/plugin-ai-profiles-administration/src/locales/fr.ts create mode 100644 webapp/packages/plugin-ai-profiles-administration/src/locales/ru.ts create mode 100644 webapp/packages/plugin-ai-profiles-administration/src/module.ts create mode 100644 webapp/packages/plugin-ai-profiles-administration/tsconfig.json create mode 100644 webapp/packages/plugin-ai-profiles/.gitignore create mode 100644 webapp/packages/plugin-ai-profiles/package.json rename webapp/packages/{plugin-ai => plugin-ai-profiles}/src/AIProfileCredentialsDialog.tsx (91%) rename webapp/packages/{plugin-ai => plugin-ai-profiles}/src/AIProfileCredentialsDialogLazy.ts (100%) rename webapp/packages/{plugin-ai => plugin-ai-profiles}/src/AIProfileCredentialsService.ts (63%) rename webapp/packages/{plugin-ai => plugin-ai-profiles}/src/AIProfileCredentialsUtils.ts (65%) rename webapp/packages/{plugin-ai/src/UserAIProfileResource.ts => plugin-ai-profiles/src/AIProfilesResource.ts} (97%) rename webapp/packages/{plugin-ai => plugin-ai-profiles}/src/IAIProfileCredentialsDialogPayload.ts (100%) rename webapp/packages/{plugin-ai => plugin-ai-profiles}/src/LocaleService.ts (100%) create mode 100644 webapp/packages/plugin-ai-profiles/src/index.ts rename webapp/packages/{plugin-ai => plugin-ai-profiles}/src/locales/en.ts (100%) rename webapp/packages/{plugin-ai => plugin-ai-profiles}/src/locales/fr.ts (100%) rename webapp/packages/{plugin-ai => plugin-ai-profiles}/src/locales/ru.ts (100%) rename webapp/packages/{plugin-ai => plugin-ai-profiles}/src/locales/zh.ts (100%) create mode 100644 webapp/packages/plugin-ai-profiles/src/module.ts create mode 100644 webapp/packages/plugin-ai-profiles/tsconfig.json diff --git a/webapp/packages/plugin-ai-administration/package.json b/webapp/packages/plugin-ai-administration/package.json index 6d762b092d5..752555eb74b 100644 --- a/webapp/packages/plugin-ai-administration/package.json +++ b/webapp/packages/plugin-ai-administration/package.json @@ -31,13 +31,11 @@ "@cloudbeaver/core-localization": "workspace:*", "@cloudbeaver/core-resource": "workspace:*", "@cloudbeaver/core-root": "workspace:*", - "@cloudbeaver/core-sdk": "workspace:*", "@cloudbeaver/core-ui": "workspace:*", "@cloudbeaver/core-utils": "workspace:*", "@cloudbeaver/plugin-ai": "workspace:*", - "@cloudbeaver/plugin-data-grid": "workspace:*", + "@cloudbeaver/plugin-ai-profiles": "workspace:*", "@dbeaver/js-helpers": "workspace:*", - "@dbeaver/ui-kit": "workspace:*", "mobx": "^6", "mobx-react-lite": "^4", "react": "^19", @@ -49,6 +47,7 @@ "@cloudbeaver/tsconfig": "workspace:*", "@types/react": "^19", "rimraf": "^6", - "typescript": "^5" + "typescript": "^5", + "typescript-plugin-css-modules": "^5" } } diff --git a/webapp/packages/plugin-ai-administration/src/AIAdministrationPage.tsx b/webapp/packages/plugin-ai-administration/src/AIAdministrationPage.tsx index cad4a668f32..68a346dcfa4 100644 --- a/webapp/packages/plugin-ai-administration/src/AIAdministrationPage.tsx +++ b/webapp/packages/plugin-ai-administration/src/AIAdministrationPage.tsx @@ -27,8 +27,8 @@ import { import { useService } from '@cloudbeaver/core-di'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; import { NotificationService } from '@cloudbeaver/core-events'; +import { AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; -import { AIAdminProfilesResource } from './AIProfiles/AIProfilesResource.js'; import { getAdministrationAISettingsFormInfoPart } from './AISettingsForm/getAdministrationAISettingsFormInfoPart.js'; import { LANGUAGE_OPTIONS } from './AISettingsForm/getLanguageOptions.js'; import type { AdministrationAISettingsFormState } from './AISettingsForm/AdministrationAISettingsFormState.js'; @@ -41,7 +41,7 @@ export const AIAdministrationPage = observer<{ }>(function AIAdministrationPage({ formState }) { const translate = useTranslate(); const notificationService = useService(NotificationService); - const profilesLoader = useResource(AIAdministrationPage, AIAdminProfilesResource, CachedMapAllKey); + const profilesLoader = useResource(AIAdministrationPage, AIProfilesResource, CachedMapAllKey); const aiEnginesResource = useResource(AIAdministrationPage, AiEnginesResource, undefined); const profiles = profilesLoader.data.filter(isDefined); diff --git a/webapp/packages/plugin-ai-administration/src/AIAdministrationProfilesTabPanel.tsx b/webapp/packages/plugin-ai-administration/src/AIAdministrationProfilesTabPanel.tsx deleted file mode 100644 index 6b0077c2990..00000000000 --- a/webapp/packages/plugin-ai-administration/src/AIAdministrationProfilesTabPanel.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 { useService } from '@cloudbeaver/core-di'; - -import { AIProfilesPanel } from './AIProfiles/AIProfilesPanel.js'; -import { AISettingsService } from './AISettingsService.js'; - -export const AIAdministrationProfilesTabPanel = observer(function AIAdministrationProfilesTabPanel() { - const aiSettingsService = useService(AISettingsService); - - if (!aiSettingsService.formState) { - return null; - } - - return ; -}); diff --git a/webapp/packages/plugin-ai-administration/src/AIAdministrationTabsService.ts b/webapp/packages/plugin-ai-administration/src/AIAdministrationTabsService.ts index 65e1ae4b365..23af420eeae 100644 --- a/webapp/packages/plugin-ai-administration/src/AIAdministrationTabsService.ts +++ b/webapp/packages/plugin-ai-administration/src/AIAdministrationTabsService.ts @@ -15,10 +15,6 @@ const AIAdministrationMainTabPanel = importLazyComponent(() => import('./AIAdministrationMainTabPanel.js').then(module => module.AIAdministrationMainTabPanel), ); -const AIAdministrationProfilesTabPanel = importLazyComponent(() => - import('./AIAdministrationProfilesTabPanel.js').then(module => module.AIAdministrationProfilesTabPanel), -); - @injectable(() => [AIAdministrationBootstrap]) export class AIAdministrationTabsService extends Bootstrap { readonly tabsContainer: TabsContainer; @@ -36,13 +32,6 @@ export class AIAdministrationTabsService extends Bootstrap { panel: () => AIAdministrationMainTabPanel, }); - this.tabsContainer.add({ - key: EAIAdministrationSub.Profiles, - name: 'plugin_ai_administration_profiles_title', - order: 2, - panel: () => AIAdministrationProfilesTabPanel, - }); - - this.aiAdministrationBootstrap.administrationItem.sub.push({ name: EAIAdministrationSub.Settings }, { name: EAIAdministrationSub.Profiles }); + this.aiAdministrationBootstrap.administrationItem.sub.push({ name: EAIAdministrationSub.Settings }); } } diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesResource.ts b/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesResource.ts deleted file mode 100644 index 55a444348be..00000000000 --- a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesResource.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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 { injectable } from '@cloudbeaver/core-di'; -import { CachedMapAllKey, CachedMapResource, resourceKeyList } from '@cloudbeaver/core-resource'; -import { EAdminPermission, ServerConfigResource, SessionPermissionsResource } from '@cloudbeaver/core-root'; -import { - GraphQLService, - type AiEngineConfig, - type AiAdminConfigurationProfileInfo, - type AiConfigurationProfileInput, - type AiModelInfo, -} from '@cloudbeaver/core-sdk'; -import { type AIProfile, UserAIProfileResource } from '@cloudbeaver/plugin-ai'; - -import { AISettingsResource } from '../AISettingsResource.js'; - -export type AIAdminProfile = AiAdminConfigurationProfileInfo; -export type AIProfileInput = AiConfigurationProfileInput; - -@injectable(() => [GraphQLService, SessionPermissionsResource, ServerConfigResource, AISettingsResource, UserAIProfileResource]) -export class AIAdminProfilesResource extends CachedMapResource { - constructor( - private readonly graphQLService: GraphQLService, - permissionsResource: SessionPermissionsResource, - serverConfigResource: ServerConfigResource, - aiSettingsResource: AISettingsResource, - private readonly userAIProfileResource: UserAIProfileResource, - ) { - super(); - - this.sync( - serverConfigResource, - () => undefined, - () => CachedMapAllKey, - ); - - permissionsResource.require(this, EAdminPermission.admin).outdateResource(this); - - this.onItemDelete.addHandler(() => aiSettingsResource.markOutdated()); - this.onItemUpdate.addHandler(() => aiSettingsResource.markOutdated()); - } - - async create(config: AIProfileInput): Promise { - const { profile } = await this.graphQLService.sdk.createAiProfile({ config }); - - this.userAIProfileResource.setProfile(profile); - this.set(profile.id, this.userAIProfileResource.get(profile.id)!); - - return profile; - } - - async update(config: AIProfileInput): Promise { - const { profile } = await this.graphQLService.sdk.updateAiProfile({ config }); - - this.userAIProfileResource.setProfile(profile); - this.set(profile.id, this.userAIProfileResource.get(profile.id)!); - - return profile; - } - - async deleteProfile(profileId: string): Promise { - await this.graphQLService.sdk.deleteAiProfile({ profileId }); - - this.userAIProfileResource.removeProfile(profileId); - this.delete(profileId); - } - - async loadModels(engineId: string, profileId?: string, settings?: AiEngineConfig): Promise { - const { models } = await this.graphQLService.sdk.getEngineModels({ engineId, profileId, settings }); - return models; - } - - protected async loader(): Promise> { - await this.userAIProfileResource.refresh(CachedMapAllKey); - const profiles = this.userAIProfileResource.values; - - const key = resourceKeyList(profiles.map(profile => profile.id)); - this.replace(key, profiles); - - return this.data; - } - - protected validateKey(key: string): boolean { - return typeof key === 'string'; - } -} diff --git a/webapp/packages/plugin-ai-administration/src/AISettingsForm/AdministrationAISettingsInfoPart.ts b/webapp/packages/plugin-ai-administration/src/AISettingsForm/AdministrationAISettingsInfoPart.ts index 51248b36766..d2a1caa69eb 100644 --- a/webapp/packages/plugin-ai-administration/src/AISettingsForm/AdministrationAISettingsInfoPart.ts +++ b/webapp/packages/plugin-ai-administration/src/AISettingsForm/AdministrationAISettingsInfoPart.ts @@ -8,9 +8,9 @@ import { CachedMapAllKey } from '@cloudbeaver/core-resource'; import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { FormPart, formValidationContext, type IFormState } from '@cloudbeaver/core-ui'; +import { AISettingsResource } from '@cloudbeaver/plugin-ai'; +import { AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; -import { AIAdminProfilesResource } from '../AIProfiles/AIProfilesResource.js'; -import type { AISettingsResource } from '../AISettingsResource.js'; import { LANGUAGE_VALIDATION_REGEX } from './getLanguageOptions.js'; import type { IAdministrationAIInfoState } from './IAdministrationAIInfoState.js'; @@ -23,7 +23,7 @@ export class AdministrationAISettingsInfoPart extends FormPart, private readonly aiSettingsResource: AISettingsResource, - private readonly aiProfilesResource: AIAdminProfilesResource, + private readonly aiProfilesResource: AIProfilesResource, ) { super(formState, DEFAULT_STATE_GETTER()); } diff --git a/webapp/packages/plugin-ai-administration/src/AISettingsForm/getAdministrationAISettingsFormInfoPart.ts b/webapp/packages/plugin-ai-administration/src/AISettingsForm/getAdministrationAISettingsFormInfoPart.ts index 05f94304b06..f621f38cf27 100644 --- a/webapp/packages/plugin-ai-administration/src/AISettingsForm/getAdministrationAISettingsFormInfoPart.ts +++ b/webapp/packages/plugin-ai-administration/src/AISettingsForm/getAdministrationAISettingsFormInfoPart.ts @@ -7,9 +7,9 @@ */ import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; import type { IFormState } from '@cloudbeaver/core-ui'; +import { AISettingsResource } from '@cloudbeaver/plugin-ai'; +import { AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; -import { AIAdminProfilesResource } from '../AIProfiles/AIProfilesResource.js'; -import { AISettingsResource } from '../AISettingsResource.js'; import { AdministrationAISettingsInfoPart } from './AdministrationAISettingsInfoPart.js'; const DATA_CONTEXT_ADMINISTRATION_AI_SETTINGS_FORM_INFO_PART = createDataContext( @@ -20,7 +20,7 @@ export function getAdministrationAISettingsFormInfoPart(formState: IFormState { const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; const aiSettingsResource = di.getService(AISettingsResource); - const aiProfilesResource = di.getService(AIAdminProfilesResource); + const aiProfilesResource = di.getService(AIProfilesResource); return new AdministrationAISettingsInfoPart(formState, aiSettingsResource, aiProfilesResource); }); diff --git a/webapp/packages/plugin-ai-administration/src/AISettingsResource.ts b/webapp/packages/plugin-ai-administration/src/AISettingsResource.ts deleted file mode 100644 index dd128c560a1..00000000000 --- a/webapp/packages/plugin-ai-administration/src/AISettingsResource.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 { injectable } from '@cloudbeaver/core-di'; -import { CachedDataResource } from '@cloudbeaver/core-resource'; -import { EAdminPermission, ServerConfigResource, SessionPermissionsResource } from '@cloudbeaver/core-root'; -import { GraphQLService, type AiSettingsConfig, type AiSettingsInfo } from '@cloudbeaver/core-sdk'; -import { isObjectsEqual } from '@cloudbeaver/core-utils'; - -@injectable(() => [GraphQLService, SessionPermissionsResource, ServerConfigResource]) -export class AISettingsResource extends CachedDataResource { - constructor( - private readonly graphQLService: GraphQLService, - permissionsResource: SessionPermissionsResource, - serverConfigResource: ServerConfigResource, - ) { - super(() => null); - - this.sync(serverConfigResource); - - permissionsResource.require(this, EAdminPermission.admin).outdateResource(this); - } - - async saveSettings(settings: AiSettingsConfig) { - await this.performUpdate(undefined, undefined, async () => { - const { result } = await this.graphQLService.sdk.saveAiSettings({ - settings, - }); - - this.setData(result); - this.onDataOutdated.execute(); - - return true; - }); - } - - isChanged(settings: AiSettingsConfig) { - if (!this.data) { - return false; - } - - return !isObjectsEqual(settings, this.data); - } - - protected async loader() { - const { settings } = await this.graphQLService.sdk.getAiSettings(); - return settings; - } -} diff --git a/webapp/packages/plugin-ai-administration/src/AISettingsService.ts b/webapp/packages/plugin-ai-administration/src/AISettingsService.ts index bf54bd443da..e66cbebaf0d 100644 --- a/webapp/packages/plugin-ai-administration/src/AISettingsService.ts +++ b/webapp/packages/plugin-ai-administration/src/AISettingsService.ts @@ -7,17 +7,20 @@ */ import { injectable, IServiceProvider } from '@cloudbeaver/core-di'; import { FormMode } from '@cloudbeaver/core-ui'; +import { AISettingsResource } from '@cloudbeaver/plugin-ai'; import { AdministrationAISettingsFormService } from './AISettingsForm/AdministrationAISettingsFormService.js'; import { AdministrationAISettingsFormState } from './AISettingsForm/AdministrationAISettingsFormState.js'; +import { getAdministrationAISettingsFormInfoPart } from './AISettingsForm/getAdministrationAISettingsFormInfoPart.js'; -@injectable(() => [AdministrationAISettingsFormService, IServiceProvider]) +@injectable(() => [AdministrationAISettingsFormService, IServiceProvider, AISettingsResource]) export class AISettingsService { formState: AdministrationAISettingsFormState | null; constructor( private readonly administrationAISettingsFormService: AdministrationAISettingsFormService, private readonly serviceProvider: IServiceProvider, + private readonly aiSettingsResource: AISettingsResource, ) { this.formState = null; } @@ -28,8 +31,14 @@ export class AISettingsService { this.formState.setMode(FormMode.Edit); } - dispose() { + dispose(): void { this.formState?.dispose(); this.formState = null; } + + isEffectiveDefaultProfile(profileId: string): boolean { + const persistedProfileId = this.aiSettingsResource.data?.defaultConfiguration; + const selectedProfileId = this.formState ? getAdministrationAISettingsFormInfoPart(this.formState).state.defaultConfiguration : null; + return profileId === persistedProfileId || profileId === selectedProfileId; + } } diff --git a/webapp/packages/plugin-ai-administration/src/index.ts b/webapp/packages/plugin-ai-administration/src/index.ts index 6d931339d7b..9d320b6330e 100644 --- a/webapp/packages/plugin-ai-administration/src/index.ts +++ b/webapp/packages/plugin-ai-administration/src/index.ts @@ -11,3 +11,4 @@ import './module.js'; export { AIAdministrationTabsService } from './AIAdministrationTabsService.js'; export { AIAdministrationNavigationService, EAIAdministrationSub } from './AIAdministrationNavigationService.js'; export { AIAdministrationBootstrap } from './AIAdministrationBootstrap.js'; +export { AISettingsService } from './AISettingsService.js'; diff --git a/webapp/packages/plugin-ai-administration/src/locales/de.ts b/webapp/packages/plugin-ai-administration/src/locales/de.ts index 61eef398ac1..b26ec76ea8f 100644 --- a/webapp/packages/plugin-ai-administration/src/locales/de.ts +++ b/webapp/packages/plugin-ai-administration/src/locales/de.ts @@ -2,15 +2,10 @@ export default [ ['ai_administration_tab_title', 'KI Einstellungen'], ['ai_administration_tab_main', 'Allgemein'], ['ai_administration_settings', 'KI Einstellungen'], - ['ai_administration_language_model_settings', 'Modelleinstellungen'], - ['ai_administration_select_language_model_selector_title', 'Modell'], ['ai_administration_settings_save_success', 'KI-Einstellungen gespeichert'], ['ai_administration_settings_save_fail', 'KI-Einstellungen konnten nicht gespeichert werden'], ['ai_administration_settings_load_fail', 'KI-Einstellungen konnten nicht geladen werden'], ['ai_administration_language_model_settings_save_fail', 'Einstellungen der KI-Engine konnten nicht gespeichert werden'], - ['ai_administration_models_refresh', 'Modelle aktualisieren'], - ['ai_administration_models_refresh_description', 'Pflichtfelder ausfüllen und dann aktualisieren, um Modelle zu laden'], - ['ai_administration_models_refresh_fail', 'Modelle konnten nicht aktualisiert werden'], ['plugin_ai_administration_rag_label', 'Nur relevante Objekte an die KI senden (Experimentell)'], [ 'plugin_ai_administration_rag_description', @@ -24,38 +19,5 @@ export default [ ['plugin_ai_administration_default_profile_label', 'Standardprofil'], ['plugin_ai_administration_default_profile_description', 'Profil, das standardmäßig für KI-Funktionen verwendet wird'], ['plugin_ai_administration_default_profile_no_profiles_title', 'No profiles available'], - ['plugin_ai_administration_default_profile_no_profiles_message', 'Create a profile in the "{alias:plugin_ai_administration_profiles_title}" tab'], - ['plugin_ai_administration_profiles_title', 'Profile'], - ['plugin_ai_administration_profiles_table_empty_placeholder', 'Keine Profile gefunden. Erstellen Sie ein neues Profil'], - ['plugin_ai_administration_profile_column_name', 'Name'], - ['plugin_ai_administration_profile_column_engine', 'Engine'], - ['plugin_ai_administration_profile_default_badge', '(Standard)'], - ['plugin_ai_administration_profile_add_tooltip', 'Neues Profil erstellen'], - ['plugin_ai_administration_profile_refresh_tooltip', 'Profilliste aktualisieren'], - ['plugin_ai_administration_profile_delete_tooltip', 'Ausgewählte Profile löschen'], - ['plugin_ai_administration_profiles_refresh_success', 'Profilliste aktualisiert'], - ['plugin_ai_administration_profiles_refresh_error', 'Profilliste konnte nicht aktualisiert werden'], - ['plugin_ai_administration_profile_delete_confirmation', 'Sie sind dabei, folgende Profile zu löschen: '], - ['plugin_ai_administration_profile_delete_user_credentials_warning', 'Gespeicherte Anmeldedaten für alle Benutzer dieser Profile werden ebenfalls gelöscht.'], - ['plugin_ai_administration_profile_delete_success', 'Ausgewählte Profile gelöscht'], - ['plugin_ai_administration_profile_delete_error', 'Profile konnten nicht gelöscht werden'], - ['plugin_ai_administration_profile_created', 'Profil erstellt'], - ['plugin_ai_administration_profile_updated', 'Profil aktualisiert'], - ['plugin_ai_administration_profile_create_error', 'Profil konnte nicht erstellt werden'], - ['plugin_ai_administration_profile_save_error', 'Profil konnte nicht gespeichert werden'], - ['plugin_ai_administration_profile_form_field_name', 'Profilname'], - ['plugin_ai_administration_profile_form_field_engine', 'Engine'], - ['plugin_ai_administration_profile_profile_type', 'Quelle der Anmeldedaten'], - ['plugin_ai_administration_profile_global_credentials', 'Globale Anmeldedaten'], - ['plugin_ai_administration_profile_user_credentials', 'Benutzeranmeldedaten'], - ['plugin_ai_administration_profile_user_credentials_unsupported', 'Diese Engine unterstützt keine vom Benutzer bereitgestellten API-Token'], - ['plugin_ai_administration_profile_form_tab_options', 'Profil'], - ['plugin_ai_administration_profile_name_max_length', 'Der Profilname darf {arg:length} Zeichen nicht überschreiten'], - ['plugin_ai_administration_profile_name_min_length', 'Der Profilname muss mindestens {arg:length} Zeichen lang sein'], - [ - 'plugin_ai_administration_profile_default_delete_info', - 'Das Standardprofil kann nicht gelöscht werden. Bitte wählen Sie ein anderes Profil als Standard aus, bevor Sie dieses Profil löschen.', - ], - ['plugin_ai_administration_profile_create', 'Neues Profil'], - ['plugin_ai_administration_profile_edit', 'Profil bearbeiten'], + ['plugin_ai_administration_default_profile_no_profiles_message', 'Erstellen Sie ein Profil, bevor Sie ein Standardprofil auswählen'], ]; diff --git a/webapp/packages/plugin-ai-administration/src/locales/en.ts b/webapp/packages/plugin-ai-administration/src/locales/en.ts index f1480a933a8..9fbffa7c2be 100644 --- a/webapp/packages/plugin-ai-administration/src/locales/en.ts +++ b/webapp/packages/plugin-ai-administration/src/locales/en.ts @@ -2,15 +2,10 @@ export default [ ['ai_administration_tab_title', 'AI Settings'], ['ai_administration_tab_main', 'Main'], ['ai_administration_settings', 'AI Settings'], - ['ai_administration_language_model_settings', 'Model settings'], - ['ai_administration_select_language_model_selector_title', 'Model'], ['ai_administration_settings_save_success', 'AI settings saved'], ['ai_administration_settings_save_fail', 'Failed to save AI settings'], ['ai_administration_settings_load_fail', 'Failed to load AI settings'], ['ai_administration_language_model_settings_save_fail', 'Failed to save engine settings'], - ['ai_administration_models_refresh', 'Refresh models'], - ['ai_administration_models_refresh_description', 'Fill in required fields, then refresh to load models'], - ['ai_administration_models_refresh_fail', 'Failed to refresh models'], ['plugin_ai_administration_rag_label', 'Send only relevant objects to AI (Experimental)'], [ 'plugin_ai_administration_rag_description', @@ -24,38 +19,5 @@ export default [ ['plugin_ai_administration_default_profile_label', 'Default profile'], ['plugin_ai_administration_default_profile_description', 'Profile used by default for AI features'], ['plugin_ai_administration_default_profile_no_profiles_title', 'No profiles available'], - ['plugin_ai_administration_default_profile_no_profiles_message', 'Create a profile in the "{alias:plugin_ai_administration_profiles_title}" tab'], - ['plugin_ai_administration_profiles_title', 'Profiles'], - ['plugin_ai_administration_profiles_table_empty_placeholder', 'No profiles found. Create a new profile'], - ['plugin_ai_administration_profile_column_name', 'Name'], - ['plugin_ai_administration_profile_column_engine', 'Engine'], - ['plugin_ai_administration_profile_default_badge', '(Default)'], - ['plugin_ai_administration_profile_add_tooltip', 'Create new profile'], - ['plugin_ai_administration_profile_refresh_tooltip', 'Refresh profiles list'], - ['plugin_ai_administration_profile_delete_tooltip', 'Delete selected profiles'], - ['plugin_ai_administration_profiles_refresh_success', 'Profiles list updated'], - ['plugin_ai_administration_profiles_refresh_error', 'Failed to refresh profiles list'], - ['plugin_ai_administration_profile_delete_confirmation', 'You are about to delete profile(s): '], - ['plugin_ai_administration_profile_delete_user_credentials_warning', 'Saved credentials for all users of these profiles will also be deleted.'], - ['plugin_ai_administration_profile_delete_success', 'Selected profiles deleted'], - ['plugin_ai_administration_profile_delete_error', 'Failed to delete profiles'], - ['plugin_ai_administration_profile_created', 'Profile created'], - ['plugin_ai_administration_profile_updated', 'Profile updated'], - ['plugin_ai_administration_profile_create_error', 'Failed to create profile'], - ['plugin_ai_administration_profile_save_error', 'Failed to save profile'], - ['plugin_ai_administration_profile_form_field_name', 'Profile name'], - ['plugin_ai_administration_profile_form_field_engine', 'Engine'], - ['plugin_ai_administration_profile_profile_type', 'Credential source'], - ['plugin_ai_administration_profile_global_credentials', 'Global credentials'], - ['plugin_ai_administration_profile_user_credentials', 'User credentials'], - ['plugin_ai_administration_profile_user_credentials_unsupported', 'This engine does not support user-provided API tokens'], - ['plugin_ai_administration_profile_form_tab_options', 'Profile'], - ['plugin_ai_administration_profile_name_max_length', 'Profile name must not exceed {arg:length} characters'], - ['plugin_ai_administration_profile_name_min_length', 'Profile name must be at least {arg:length} characters'], - [ - 'plugin_ai_administration_profile_default_delete_info', - 'The default profile cannot be deleted. Please select another profile as the default before deleting this profile.', - ], - ['plugin_ai_administration_profile_create', 'New Profile'], - ['plugin_ai_administration_profile_edit', 'Edit Profile'], + ['plugin_ai_administration_default_profile_no_profiles_message', 'Create a profile before selecting a default profile'], ]; diff --git a/webapp/packages/plugin-ai-administration/src/locales/fr.ts b/webapp/packages/plugin-ai-administration/src/locales/fr.ts index b66643d52b2..4c02d81260d 100644 --- a/webapp/packages/plugin-ai-administration/src/locales/fr.ts +++ b/webapp/packages/plugin-ai-administration/src/locales/fr.ts @@ -2,15 +2,10 @@ export default [ ['ai_administration_tab_title', "Paramètres d'IA"], ['ai_administration_tab_main', 'Principal'], ['ai_administration_settings', "Paramètres d'IA"], - ['ai_administration_language_model_settings', 'Paramètres du modèle'], - ['ai_administration_select_language_model_selector_title', 'Modèle'], ['ai_administration_settings_save_success', "Les paramètres de l'IA ont été sauvegardé"], ['ai_administration_settings_save_fail', "Échec de l'enregistrement des réglages de l'IA"], ['ai_administration_settings_load_fail', "Échec du chargement des paramètres de l'IA"], ['ai_administration_language_model_settings_save_fail', "Échec de l'enregistrement des réglages du modèle"], - ['ai_administration_models_refresh', 'Actualiser les modèles'], - ['ai_administration_models_refresh_description', 'Remplissez les champs requis, puis actualisez pour charger les modèles'], - ['ai_administration_models_refresh_fail', "Échec de l'actualisation des modèles"], ['plugin_ai_administration_rag_label', 'Envoyer uniquement les objets pertinents à l’IA (Expérimental)'], [ 'plugin_ai_administration_rag_description', @@ -24,38 +19,5 @@ export default [ ['plugin_ai_administration_default_profile_label', 'Profil par défaut'], ['plugin_ai_administration_default_profile_description', "Profil utilisé par défaut pour les fonctionnalités d'IA"], ['plugin_ai_administration_default_profile_no_profiles_title', 'No profiles available'], - ['plugin_ai_administration_default_profile_no_profiles_message', 'Create a profile in the "{alias:plugin_ai_administration_profiles_title}" tab'], - ['plugin_ai_administration_profiles_title', 'Profils'], - ['plugin_ai_administration_profiles_table_empty_placeholder', 'Aucun profil trouvé. Créez un nouveau profil'], - ['plugin_ai_administration_profile_column_name', 'Nom'], - ['plugin_ai_administration_profile_column_engine', "Modèle d'IA"], - ['plugin_ai_administration_profile_default_badge', '(Par défaut)'], - ['plugin_ai_administration_profile_add_tooltip', 'Créer un nouveau profil'], - ['plugin_ai_administration_profile_refresh_tooltip', 'Actualiser la liste des profils'], - ['plugin_ai_administration_profile_delete_tooltip', 'Supprimer les profils sélectionnés'], - ['plugin_ai_administration_profiles_refresh_success', 'Liste des profils mise à jour'], - ['plugin_ai_administration_profiles_refresh_error', 'Échec de l’actualisation de la liste des profils'], - ['plugin_ai_administration_profile_delete_confirmation', 'Vous êtes sur le point de supprimer le(s) profil(s) : '], - ['plugin_ai_administration_profile_delete_user_credentials_warning', 'Les identifiants enregistrés pour tous les utilisateurs de ces profils seront également supprimés.'], - ['plugin_ai_administration_profile_delete_success', 'Profils sélectionnés supprimés'], - ['plugin_ai_administration_profile_delete_error', 'Échec de la suppression des profils'], - ['plugin_ai_administration_profile_created', 'Profil créé'], - ['plugin_ai_administration_profile_updated', 'Profil mis à jour'], - ['plugin_ai_administration_profile_create_error', 'Échec de la création du profil'], - ['plugin_ai_administration_profile_save_error', 'Échec de l’enregistrement du profil'], - ['plugin_ai_administration_profile_form_field_name', 'Nom du profil'], - ['plugin_ai_administration_profile_form_field_engine', "Modèle d'IA"], - ['plugin_ai_administration_profile_profile_type', 'Source des identifiants'], - ['plugin_ai_administration_profile_global_credentials', 'Identifiants globaux'], - ['plugin_ai_administration_profile_user_credentials', 'Identifiants utilisateur'], - ['plugin_ai_administration_profile_user_credentials_unsupported', 'Ce moteur ne prend pas en charge les jetons API fournis par les utilisateurs'], - ['plugin_ai_administration_profile_form_tab_options', 'Profil'], - ['plugin_ai_administration_profile_name_max_length', 'Le nom du profil ne doit pas dépasser {arg:length} caractères'], - ['plugin_ai_administration_profile_name_min_length', 'Le nom du profil doit contenir au moins {arg:length} caractères'], - [ - 'plugin_ai_administration_profile_default_delete_info', - 'Le profil par défaut ne peut pas être supprimé. Veuillez sélectionner un autre profil comme profil par défaut avant de supprimer ce profil.', - ], - ['plugin_ai_administration_profile_create', 'Nouveau profil'], - ['plugin_ai_administration_profile_edit', 'Modifier le profil'], + ['plugin_ai_administration_default_profile_no_profiles_message', 'Créez un profil avant de sélectionner un profil par défaut'], ]; diff --git a/webapp/packages/plugin-ai-administration/src/locales/ru.ts b/webapp/packages/plugin-ai-administration/src/locales/ru.ts index e90981343d9..23d8f703a96 100644 --- a/webapp/packages/plugin-ai-administration/src/locales/ru.ts +++ b/webapp/packages/plugin-ai-administration/src/locales/ru.ts @@ -2,15 +2,10 @@ export default [ ['ai_administration_tab_title', 'AI Настройки'], ['ai_administration_tab_main', 'Основные'], ['ai_administration_settings', 'AI Настройки'], - ['ai_administration_language_model_settings', 'Настройки модели'], - ['ai_administration_select_language_model_selector_title', 'Модель'], ['ai_administration_settings_save_success', 'Настройки AI сохранены'], ['ai_administration_settings_save_fail', 'Не удалось сохранить настройки AI'], ['ai_administration_settings_load_fail', 'Не удалось загрузить настройки AI'], ['ai_administration_language_model_settings_save_fail', 'Не удалось сохранить настройки энджина'], - ['ai_administration_models_refresh', 'Обновить модели'], - ['ai_administration_models_refresh_description', 'Заполните обязательные поля и обновите список моделей'], - ['ai_administration_models_refresh_fail', 'Не удалось обновить модели'], ['plugin_ai_administration_rag_label', 'Отправлять в ИИ только релевантные объекты (Экспериментально)'], [ 'plugin_ai_administration_rag_description', @@ -24,38 +19,5 @@ export default [ ['plugin_ai_administration_default_profile_label', 'Профиль по умолчанию'], ['plugin_ai_administration_default_profile_description', 'Профиль, используемый по умолчанию для функций ИИ'], ['plugin_ai_administration_default_profile_no_profiles_title', 'Нет доступных профилей'], - ['plugin_ai_administration_default_profile_no_profiles_message', 'Создайте профиль на вкладке "{alias:plugin_ai_administration_profiles_title}"'], - ['plugin_ai_administration_profiles_title', 'Профили'], - ['plugin_ai_administration_profiles_table_empty_placeholder', 'Профили не найдены. Создайте новый профиль'], - ['plugin_ai_administration_profile_column_name', 'Название'], - ['plugin_ai_administration_profile_column_engine', 'Энджин'], - ['plugin_ai_administration_profile_default_badge', '(По умолчанию)'], - ['plugin_ai_administration_profile_add_tooltip', 'Создать новый профиль'], - ['plugin_ai_administration_profile_refresh_tooltip', 'Обновить список профилей'], - ['plugin_ai_administration_profile_delete_tooltip', 'Удалить выбранные профили'], - ['plugin_ai_administration_profiles_refresh_success', 'Список профилей обновлён'], - ['plugin_ai_administration_profiles_refresh_error', 'Не удалось обновить список профилей'], - ['plugin_ai_administration_profile_delete_confirmation', 'Вы собираетесь удалить профили(ь): '], - ['plugin_ai_administration_profile_delete_user_credentials_warning', 'Сохраненные учетные данные всех пользователей этих профилей также будут удалены.'], - ['plugin_ai_administration_profile_delete_success', 'Выбранные профили удалены'], - ['plugin_ai_administration_profile_delete_error', 'Не удалось удалить профили'], - ['plugin_ai_administration_profile_created', 'Профиль создан'], - ['plugin_ai_administration_profile_updated', 'Профиль обновлён'], - ['plugin_ai_administration_profile_create_error', 'Не удалось создать профиль'], - ['plugin_ai_administration_profile_save_error', 'Не удалось сохранить профиль'], - ['plugin_ai_administration_profile_form_field_name', 'Название профиля'], - ['plugin_ai_administration_profile_form_field_engine', 'Энджин'], - ['plugin_ai_administration_profile_profile_type', 'Источник учетных данных'], - ['plugin_ai_administration_profile_global_credentials', 'Глобальные учетные данные'], - ['plugin_ai_administration_profile_user_credentials', 'Учетные данные пользователя'], - ['plugin_ai_administration_profile_user_credentials_unsupported', 'Этот движок не поддерживает API-токены, предоставляемые пользователями'], - ['plugin_ai_administration_profile_form_tab_options', 'Профиль'], - ['plugin_ai_administration_profile_name_max_length', 'Название профиля не должно превышать {arg:length} символов'], - ['plugin_ai_administration_profile_name_min_length', 'Название профиля должно содержать не менее {arg:length} символов'], - [ - 'plugin_ai_administration_profile_default_delete_info', - 'Профиль по умолчанию нельзя удалить. Перед удалением этого профиля выберите другой профиль по умолчанию.', - ], - ['plugin_ai_administration_profile_create', 'Новый профиль'], - ['plugin_ai_administration_profile_edit', 'Редактировать профиль'], + ['plugin_ai_administration_default_profile_no_profiles_message', 'Создайте профиль перед выбором профиля по умолчанию'], ]; diff --git a/webapp/packages/plugin-ai-administration/src/module.ts b/webapp/packages/plugin-ai-administration/src/module.ts index dbb38d7fccf..77d91b8774d 100644 --- a/webapp/packages/plugin-ai-administration/src/module.ts +++ b/webapp/packages/plugin-ai-administration/src/module.ts @@ -6,14 +6,9 @@ * you may not use this file except in compliance with the License. */ -import { Bootstrap, Dependency, ModuleRegistry, proxy } from '@cloudbeaver/core-di'; +import { Bootstrap, ModuleRegistry, proxy } from '@cloudbeaver/core-di'; import { LocaleService } from './LocaleService.js'; -import { AISettingsResource } from './AISettingsResource.js'; import { AISettingsService } from './AISettingsService.js'; -import { AIEnginePropertiesResource } from './AIProfiles/AIEnginePropertiesResource.js'; -import { AIAdminProfilesResource } from './AIProfiles/AIProfilesResource.js'; -import { AIProfileFormService } from './AIProfiles/AIProfileForm/AIProfileFormService.js'; -import { AIProfileFormTabBootstrap } from './AIProfiles/AIProfileForm/AIProfileFormTabBootstrap.js'; import { AdministrationAISettingsFormService } from './AISettingsForm/AdministrationAISettingsFormService.js'; import { AIAdministrationBootstrap } from './AIAdministrationBootstrap.js'; import { AIAdministrationTabsService } from './AIAdministrationTabsService.js'; @@ -28,15 +23,7 @@ export default ModuleRegistry.add({ .addSingleton(AIAdministrationBootstrap) .addSingleton(Bootstrap, LocaleService) .addSingleton(Bootstrap, proxy(AIAdministrationTabsService)) - .addSingleton(Dependency, proxy(AISettingsResource)) - .addSingleton(Dependency, proxy(AIAdminProfilesResource)) - .addSingleton(Dependency, proxy(AIEnginePropertiesResource)) - .addSingleton(AISettingsResource) .addSingleton(AISettingsService) - .addSingleton(AIAdminProfilesResource) - .addSingleton(AIEnginePropertiesResource) - .addSingleton(AIProfileFormService) - .addSingleton(Bootstrap, AIProfileFormTabBootstrap) .addSingleton(AIAdministrationTabsService) .addSingleton(AdministrationAISettingsFormService) .addSingleton(AIAdministrationNavigationService); diff --git a/webapp/packages/plugin-ai-administration/tsconfig.json b/webapp/packages/plugin-ai-administration/tsconfig.json index 5387aa12e75..15c8723f666 100644 --- a/webapp/packages/plugin-ai-administration/tsconfig.json +++ b/webapp/packages/plugin-ai-administration/tsconfig.json @@ -11,9 +11,6 @@ "**/node_modules" ], "references": [ - { - "path": "../../common-react/@dbeaver/ui-kit" - }, { "path": "../../common-typescript/@dbeaver/js-helpers" }, @@ -50,9 +47,6 @@ { "path": "../core-root" }, - { - "path": "../core-sdk" - }, { "path": "../core-ui" }, @@ -63,7 +57,7 @@ "path": "../plugin-ai" }, { - "path": "../plugin-data-grid" + "path": "../plugin-ai-profiles" } ], "include": [ diff --git a/webapp/packages/plugin-ai-chat/package.json b/webapp/packages/plugin-ai-chat/package.json index 20a2b190edd..b1a07b7590c 100644 --- a/webapp/packages/plugin-ai-chat/package.json +++ b/webapp/packages/plugin-ai-chat/package.json @@ -40,6 +40,7 @@ "@cloudbeaver/core-utils": "workspace:*", "@cloudbeaver/core-view": "workspace:*", "@cloudbeaver/plugin-ai": "workspace:*", + "@cloudbeaver/plugin-ai-profiles": "workspace:*", "@cloudbeaver/plugin-codemirror6": "workspace:*", "@cloudbeaver/plugin-datasource-context-switch": "workspace:*", "@cloudbeaver/plugin-navigation-tabs": "workspace:*", diff --git a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationScope/AIChatConversationProfile.tsx b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationScope/AIChatConversationProfile.tsx index a34b3689eac..8af77101543 100644 --- a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationScope/AIChatConversationProfile.tsx +++ b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationScope/AIChatConversationProfile.tsx @@ -13,7 +13,8 @@ import { ActionIconButton, IconOrImage, RadioIndicator, useResource, useTranslat import { useService } from '@cloudbeaver/core-di'; import { DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; -import { AIProfileCredentialsDialogService, AiEnginesResource, requiresUserCredentials, type AIProfile } from '@cloudbeaver/plugin-ai'; +import { AiEnginesResource } from '@cloudbeaver/plugin-ai'; +import { AIProfileCredentialsService, type AIProfile } from '@cloudbeaver/plugin-ai-profiles'; import type { AIChatConversationInfo } from '../AIChatConversationsResource.js'; import { AIChatConversationsService } from '../AIChatConversationsService.js'; @@ -28,16 +29,16 @@ export const AIChatConversationProfile = observer(function AIChatConversa const translate = useTranslate(); const notificationService = useService(NotificationService); const aiChatConversationsService = useService(AIChatConversationsService); - const credentialsDialogService = useService(AIProfileCredentialsDialogService); + const credentialsService = useService(AIProfileCredentialsService); const menu = useMenuContext(); const aiEnginesResource = useResource(AIChatConversationProfile, AiEnginesResource, undefined); async function selectProfile(profile: AIProfile) { try { - if (requiresUserCredentials(profile)) { + if (credentialsService.isRequired(profile)) { menu?.hide(); - const { status } = await credentialsDialogService.open(profile.id); + const { status } = await credentialsService.open(profile.id); if (status !== DialogueStateResult.Resolved) { return; } @@ -52,7 +53,7 @@ export const AIChatConversationProfile = observer(function AIChatConversa event.stopPropagation(); menu?.hide(); try { - await credentialsDialogService.open(profileId); + await credentialsService.open(profileId); } catch (exception: any) { notificationService.logException(exception, 'plugin_ai_chat_profile_credentials_edit_fail'); } diff --git a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationScope/AIChatConversationScope.tsx b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationScope/AIChatConversationScope.tsx index dffabcabe4e..aa322c879a3 100644 --- a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationScope/AIChatConversationScope.tsx +++ b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationScope/AIChatConversationScope.tsx @@ -17,7 +17,7 @@ import { ConnectionsManagerService, ContainerResource } from '@cloudbeaver/core- import { NotificationService } from '@cloudbeaver/core-events'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; import { AiDatabaseScope } from '@cloudbeaver/core-sdk'; -import { UserAIProfileResource } from '@cloudbeaver/plugin-ai'; +import { AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; import { AIChatConversationScopeCustomDialog } from './AIChatConversationScopeCustom/AIChatConversationScopeCustomDialog.js'; import { AIChatConversationProfile } from './AIChatConversationProfile.js'; @@ -46,7 +46,7 @@ export const AIChatConversationScope = observer(function AIChatConversati const { data: container } = useResource(AIChatConversationScope, ContainerResource, conversation.dataSourceId ?? null); const { data: currentScope } = useResource(AIChatConversationScope, AIChatConversationScopeResource, conversation.id); - const { data: profileList } = useResource(AIChatConversationScope, UserAIProfileResource, CachedMapAllKey); + const { data: profileList } = useResource(AIChatConversationScope, AIProfilesResource, CachedMapAllKey); const profiles = profileList.filter(profile => profile !== undefined); async function selectScope(scope: AiDatabaseScope) { diff --git a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationsResource.ts b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationsResource.ts index 8b2562dcc86..dcb44ba70ba 100644 --- a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationsResource.ts +++ b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationsResource.ts @@ -18,7 +18,8 @@ import { type ResourceKey, } from '@cloudbeaver/core-resource'; import { type AiChatConversationFragment, type AiChatConversationInput, GraphQLService } from '@cloudbeaver/core-sdk'; -import { AISettingsResource, UserAIProfileResource } from '@cloudbeaver/plugin-ai'; +import { AISettingsResource } from '@cloudbeaver/plugin-ai'; +import { AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; import type { EAIConversationPromptGeneratorId } from '../../EAIConversationPromptGeneratorId.js'; @@ -33,12 +34,12 @@ export const ChatConversationConnectionKey = resourceKeyListAliasFactory( }), ); -@injectable(() => [GraphQLService, UserInfoResource, UserAIProfileResource, AISettingsResource]) +@injectable(() => [GraphQLService, UserInfoResource, AIProfilesResource, AISettingsResource]) export class AIChatConversationsResource extends CachedMapResource { constructor( private readonly graphQLService: GraphQLService, userInfoResource: UserInfoResource, - userAIProfileResource: UserAIProfileResource, + aiProfilesResource: AIProfilesResource, aiSettingsResource: AISettingsResource, ) { super(); @@ -47,7 +48,7 @@ export class AIChatConversationsResource extends CachedMapResource { + aiProfilesResource.onItemDelete.addHandler(async key => { const deletedProfileIds = ResourceKeyUtils.toArray(key); const conversations = this.values.filter(conversation => conversation.profile && deletedProfileIds.includes(conversation.profile)); if (conversations.length === 0) { diff --git a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/AIChatMessageService.ts b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/AIChatMessageService.ts index 232ab2e7b79..48e73cd7bf8 100644 --- a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/AIChatMessageService.ts +++ b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/AIChatMessageService.ts @@ -15,7 +15,8 @@ import { LocalizationService } from '@cloudbeaver/core-localization'; import { Executor, ExecutorInterrupter } from '@cloudbeaver/core-executor'; import { ConnectionsManagerService, type IConnectionInfoParams } from '@cloudbeaver/core-connections'; import type { AiSendChatMessageInfoFragment } from '@cloudbeaver/core-sdk'; -import { AIProfileCredentialsDialogService, AISettingsResource, requiresUserCredentials, UserAIProfileResource } from '@cloudbeaver/plugin-ai'; +import { AISettingsResource } from '@cloudbeaver/plugin-ai'; +import { AIProfileCredentialsService, AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; import { AIChatMessagesResource, isFunctionConfirmationMessage, isFunctionMessage, type IMessageParam } from './AIChatMessagesResource.js'; import { AIChatConversationsResource } from '../AIChatConversation/AIChatConversationsResource.js'; @@ -51,9 +52,9 @@ type MessageSendExecutorData = IMessageSendExecutorBeforeData | IMessageSendExec CommonDialogService, LocalizationService, ConnectionsManagerService, - UserAIProfileResource, + AIProfilesResource, AISettingsResource, - AIProfileCredentialsDialogService, + AIProfileCredentialsService, ]) export class AIChatMessageService { onMessageSend: Executor; @@ -64,9 +65,9 @@ export class AIChatMessageService { private readonly commonDialogService: CommonDialogService, private readonly localizationService: LocalizationService, private readonly connectionsManagerService: ConnectionsManagerService, - private readonly userAIProfileResource: UserAIProfileResource, + private readonly aiProfilesResource: AIProfilesResource, private readonly aiSettingsResource: AISettingsResource, - private readonly credentialsDialogService: AIProfileCredentialsDialogService, + private readonly credentialsService: AIProfileCredentialsService, ) { this.onMessageSend = new Executor(); @@ -129,19 +130,19 @@ export class AIChatMessageService { const conversation = await this.aiChatConversationsResource.load(conversationId); const settings = await this.aiSettingsResource.load(); let profileId = conversation.profile ?? settings?.defaultConfiguration; - let profile = profileId ? await this.userAIProfileResource.load(profileId) : undefined; + let profile = profileId ? await this.aiProfilesResource.load(profileId) : undefined; if (!profile && conversation.profile && settings?.defaultConfiguration && conversation.profile !== settings.defaultConfiguration) { profileId = settings.defaultConfiguration; - profile = await this.userAIProfileResource.load(profileId); + profile = await this.aiProfilesResource.load(profileId); if (profile) { await this.aiChatConversationsResource.updateConversation(conversation.id, { settings: { profile: profileId } }); } } if (profileId) { - if (profile && requiresUserCredentials(profile)) { - const { status } = await this.credentialsDialogService.open(profile.id); + if (profile && this.credentialsService.isRequired(profile)) { + const { status } = await this.credentialsService.open(profile.id); if (status !== DialogueStateResult.Resolved) { return; } diff --git a/webapp/packages/plugin-ai-chat/tsconfig.json b/webapp/packages/plugin-ai-chat/tsconfig.json index 64f4bca7ce2..a4a84363b4d 100644 --- a/webapp/packages/plugin-ai-chat/tsconfig.json +++ b/webapp/packages/plugin-ai-chat/tsconfig.json @@ -73,6 +73,9 @@ { "path": "../plugin-ai" }, + { + "path": "../plugin-ai-profiles" + }, { "path": "../plugin-codemirror6" }, diff --git a/webapp/packages/plugin-ai-profiles-administration/.gitignore b/webapp/packages/plugin-ai-profiles-administration/.gitignore new file mode 100644 index 00000000000..12c18d4eded --- /dev/null +++ b/webapp/packages/plugin-ai-profiles-administration/.gitignore @@ -0,0 +1 @@ +/lib/ diff --git a/webapp/packages/plugin-ai-profiles-administration/package.json b/webapp/packages/plugin-ai-profiles-administration/package.json new file mode 100644 index 00000000000..f961d0ee61d --- /dev/null +++ b/webapp/packages/plugin-ai-profiles-administration/package.json @@ -0,0 +1,57 @@ +{ + "name": "@cloudbeaver/plugin-ai-profiles-administration", + "type": "module", + "sideEffects": [ + "./lib/module.js", + "./lib/index.js", + "src/**/*.css", + "public/**/*" + ], + "version": "0.1.0", + "description": "", + "license": "Apache-2.0", + "exports": { + ".": "./lib/index.js", + "./module": "./lib/module.js" + }, + "scripts": { + "build": "tsc -b", + "clean": "rimraf --glob lib", + "lint": "eslint ./src/ --ext .ts,.tsx", + "validate-dependencies": "core-cli-validate-dependencies" + }, + "dependencies": { + "@cloudbeaver/core-administration": "workspace:*", + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-data-context": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-dialogs": "workspace:*", + "@cloudbeaver/core-events": "workspace:*", + "@cloudbeaver/core-executor": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "@cloudbeaver/core-resource": "workspace:*", + "@cloudbeaver/core-root": "workspace:*", + "@cloudbeaver/core-sdk": "workspace:*", + "@cloudbeaver/core-ui": "workspace:*", + "@cloudbeaver/core-utils": "workspace:*", + "@cloudbeaver/plugin-ai": "workspace:*", + "@cloudbeaver/plugin-ai-administration": "workspace:*", + "@cloudbeaver/plugin-ai-profiles": "workspace:*", + "@cloudbeaver/plugin-data-grid": "workspace:*", + "@dbeaver/js-helpers": "workspace:*", + "@dbeaver/ui-kit": "workspace:*", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "tslib": "^2" + }, + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@types/react": "^19", + "rimraf": "^6", + "typescript": "^5", + "typescript-plugin-css-modules": "^5" + } +} diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIEnginePropertiesResource.ts b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIEnginePropertiesResource.ts similarity index 100% rename from webapp/packages/plugin-ai-administration/src/AIProfiles/AIEnginePropertiesResource.ts rename to webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIEnginePropertiesResource.ts diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/AIProfileForm.tsx b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/AIProfileForm.tsx similarity index 100% rename from webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/AIProfileForm.tsx rename to webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/AIProfileForm.tsx diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/AIProfileFormPanel.tsx b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/AIProfileFormPanel.tsx similarity index 100% rename from webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/AIProfileFormPanel.tsx rename to webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/AIProfileFormPanel.tsx diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/AIProfileFormService.ts b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/AIProfileFormService.ts similarity index 100% rename from webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/AIProfileFormService.ts rename to webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/AIProfileFormService.ts diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/AIProfileFormTabBootstrap.ts b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/AIProfileFormTabBootstrap.ts similarity index 100% rename from webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/AIProfileFormTabBootstrap.ts rename to webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/AIProfileFormTabBootstrap.ts diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/IAIProfileFormProps.ts b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/IAIProfileFormProps.ts similarity index 100% rename from webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/IAIProfileFormProps.ts rename to webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/IAIProfileFormProps.ts diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/IAIProfileFormState.ts b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/IAIProfileFormState.ts similarity index 100% rename from webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/IAIProfileFormState.ts rename to webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/IAIProfileFormState.ts diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/Options/AIProfileFormPart.ts b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfileFormPart.ts similarity index 88% rename from webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/Options/AIProfileFormPart.ts rename to webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfileFormPart.ts index d1d7f75bcf9..34c3b7a80bf 100644 --- a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/Options/AIProfileFormPart.ts +++ b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfileFormPart.ts @@ -11,10 +11,10 @@ import { FormMode, FormPart, formValidationContext, type IFormState } from '@clo import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; import type { AiEngineConfig } from '@cloudbeaver/core-sdk'; import { getUniqueName, trimObjectValues } from '@cloudbeaver/core-utils'; -import { supportsUserCredentials } from '@cloudbeaver/plugin-ai'; +import { AIProfileCredentialsService, AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; import { AIEnginePropertiesResource } from '../../AIEnginePropertiesResource.js'; -import { AIAdminProfilesResource, type AIAdminProfile, type AIProfileInput } from '../../AIProfilesResource.js'; +import { AIProfilesAdministrationService, type AIAdminProfile, type AIProfileInput } from '../../AIProfilesAdministrationService.js'; import { getObjectPropertiesValues } from '../../utils/getObjectPropertiesValues.js'; import { prepareProperties } from '../../utils/prepareProperties.js'; import type { IAIProfileFormState } from '../IAIProfileFormState.js'; @@ -32,7 +32,9 @@ const getDefaultState = (): IAIProfileOptionsState => ({ export class AIProfileFormPart extends FormPart { constructor( formState: IFormState, - private readonly aiProfilesResource: AIAdminProfilesResource, + private readonly aiProfilesResource: AIProfilesResource, + private readonly aiProfilesAdministrationService: AIProfilesAdministrationService, + private readonly aiProfileCredentialsService: AIProfileCredentialsService, private readonly aiEnginePropertiesResource: AIEnginePropertiesResource, ) { super(formState, getDefaultState()); @@ -86,7 +88,7 @@ export class AIProfileFormPart extends FormPart, contexts: IExecutionContextProvider>): void { const properties = this.aiEnginePropertiesResource.get(this.state.engineId) ?? []; - if (!this.state.global && !supportsUserCredentials(properties)) { + if (!this.state.global && !this.aiProfileCredentialsService.isSupported(properties)) { contexts.getContext(formValidationContext).error('plugin_ai_administration_profile_user_credentials_unsupported'); } } @@ -134,10 +136,10 @@ export class AIProfileFormPart extends FormPart = observer(function AIProfileOptions({ formState }) { const translate = useTranslate(); const notificationService = useService(NotificationService); - const aiProfilesResource = useService(AIAdminProfilesResource); + const aiProfileCredentialsService = useService(AIProfileCredentialsService); + const aiProfilesAdministrationService = useService(AIProfilesAdministrationService); const enginesLoader = useResource(AIProfileOptions, AiEnginesResource, undefined); const part = getAIProfileFormPart(formState); const propertiesLoader = useResource(AIProfileOptions, AIEnginePropertiesResource, part.state.engineId || null); const propertiesInfo = propertiesLoader.data ?? []; const usesUserCredentials = !part.state.global; - const configurableProperties = requireGlobalProfileToken( - propertiesInfo.filter(property => property.id !== 'global' && (!usesUserCredentials || property.id !== 'token')), - part.state.global, - ); + const configurableProperties = propertiesInfo + .filter(property => property.id !== 'global' && (!usesUserCredentials || property.id !== 'token')) + .map(property => (part.state.global && property.id === 'token' ? { ...property, required: true } : property)); const isEditMode = formState.mode === FormMode.Edit; const [isLoading, setIsLoading] = useState(false); const [models, setModels] = useState(null); @@ -79,7 +80,7 @@ export const AIProfileOptions: TabContainerPanelComponent = const modelProperty = configurableProperties[modelPropertyIndex]; const chatModels = (models ?? []).filter(model => model.features.map(feature => feature.toLowerCase()).includes('chat')); const hasModels = !!modelProperty; - const userCredentialsSupported = supportsUserCredentials(propertiesInfo); + const userCredentialsSupported = aiProfileCredentialsService.isSupported(propertiesInfo); const propertiesBeforeModel = hasModels ? configurableProperties.slice(0, modelPropertyIndex) : configurableProperties; const propertiesAfterModel = hasModels ? configurableProperties.slice(modelPropertyIndex + 1) : []; @@ -104,7 +105,7 @@ export const AIProfileOptions: TabContainerPanelComponent = try { setIsLoading(true); const profileId = formState.mode === FormMode.Edit ? formState.state.profileId : undefined; - const loadedModels = (await aiProfilesResource.loadModels(engineId, profileId, part.getCurrentEngineSettings())).toSorted((a, b) => + const loadedModels = (await aiProfilesAdministrationService.loadModels(engineId, profileId, part.getCurrentEngineSettings())).toSorted((a, b) => a.id.localeCompare(b.id), ); setModels(loadedModels); diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/Options/AIProfilePropertiesForm.tsx b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfilePropertiesForm.tsx similarity index 100% rename from webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/Options/AIProfilePropertiesForm.tsx rename to webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfilePropertiesForm.tsx diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/Options/AIProfileSchema.ts b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfileSchema.ts similarity index 100% rename from webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/Options/AIProfileSchema.ts rename to webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfileSchema.ts diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/Options/getAIProfileFormPart.ts b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/getAIProfileFormPart.ts similarity index 61% rename from webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/Options/getAIProfileFormPart.ts rename to webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/getAIProfileFormPart.ts index ed4ab3914e6..82002fc656c 100644 --- a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfileForm/Options/getAIProfileFormPart.ts +++ b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/getAIProfileFormPart.ts @@ -7,9 +7,10 @@ */ import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; import type { IFormState } from '@cloudbeaver/core-ui'; +import { AIProfileCredentialsService, AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; import { AIEnginePropertiesResource } from '../../AIEnginePropertiesResource.js'; -import { AIAdminProfilesResource } from '../../AIProfilesResource.js'; +import { AIProfilesAdministrationService } from '../../AIProfilesAdministrationService.js'; import type { IAIProfileFormState } from '../IAIProfileFormState.js'; import { AIProfileFormPart } from './AIProfileFormPart.js'; @@ -18,9 +19,17 @@ const DATA_CONTEXT_AI_PROFILE_FORM_PART = createDataContext(' export function getAIProfileFormPart(formState: IFormState): AIProfileFormPart { return formState.getPart(DATA_CONTEXT_AI_PROFILE_FORM_PART, context => { const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; - const aiProfilesResource = di.getService(AIAdminProfilesResource); + const aiProfilesResource = di.getService(AIProfilesResource); + const aiProfilesAdministrationService = di.getService(AIProfilesAdministrationService); + const aiProfileCredentialsService = di.getService(AIProfileCredentialsService); const aiEnginePropertiesResource = di.getService(AIEnginePropertiesResource); - return new AIProfileFormPart(formState, aiProfilesResource, aiEnginePropertiesResource); + return new AIProfileFormPart( + formState, + aiProfilesResource, + aiProfilesAdministrationService, + aiProfileCredentialsService, + aiEnginePropertiesResource, + ); }); } diff --git a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesAdministrationService.ts b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesAdministrationService.ts new file mode 100644 index 00000000000..7156e392009 --- /dev/null +++ b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesAdministrationService.ts @@ -0,0 +1,55 @@ +/* + * 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 { injectable } from '@cloudbeaver/core-di'; +import { + GraphQLService, + type AiAdminConfigurationProfileInfo, + type AiConfigurationProfileInput, + type AiEngineConfig, + type AiModelInfo, +} from '@cloudbeaver/core-sdk'; +import { AISettingsResource } from '@cloudbeaver/plugin-ai'; +import { AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; + +export type AIAdminProfile = AiAdminConfigurationProfileInfo; +export type AIProfileInput = AiConfigurationProfileInput; + +@injectable(() => [GraphQLService, AIProfilesResource, AISettingsResource]) +export class AIProfilesAdministrationService { + constructor( + private readonly graphQLService: GraphQLService, + private readonly aiProfilesResource: AIProfilesResource, + private readonly aiSettingsResource: AISettingsResource, + ) {} + + async create(config: AIProfileInput): Promise { + const { profile } = await this.graphQLService.sdk.createAiProfile({ config }); + this.aiProfilesResource.setProfile(profile); + this.aiSettingsResource.markOutdated(); + return profile; + } + + async update(config: AIProfileInput): Promise { + const { profile } = await this.graphQLService.sdk.updateAiProfile({ config }); + this.aiProfilesResource.setProfile(profile); + this.aiSettingsResource.markOutdated(); + return profile; + } + + async delete(profileId: string): Promise { + await this.graphQLService.sdk.deleteAiProfile({ profileId }); + this.aiProfilesResource.removeProfile(profileId); + this.aiSettingsResource.markOutdated(); + } + + async loadModels(engineId: string, profileId?: string, settings?: AiEngineConfig): Promise { + const { models } = await this.graphQLService.sdk.getEngineModels({ engineId, profileId, settings }); + return models; + } +} diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesPanel.tsx b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesPanel.tsx similarity index 74% rename from webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesPanel.tsx rename to webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesPanel.tsx index a57a1049aa5..6871dd4c78e 100644 --- a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesPanel.tsx +++ b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesPanel.tsx @@ -18,44 +18,41 @@ import { ToolsActionStyles, ToolsPanel, ToolsPanelStyles, - useAutoLoad, useResource, useTranslate, } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; +import { AISettingsResource } from '@cloudbeaver/plugin-ai'; +import { AISettingsService } from '@cloudbeaver/plugin-ai-administration'; +import { AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; import { TableSelectionContext, useTableSelection } from '@cloudbeaver/plugin-data-grid'; import { isDefined } from '@dbeaver/js-helpers'; -import type { AdministrationAISettingsFormState } from '../AISettingsForm/AdministrationAISettingsFormState.js'; -import { getAdministrationAISettingsFormInfoPart } from '../AISettingsForm/getAdministrationAISettingsFormInfoPart.js'; import { AIProfileFormService } from './AIProfileForm/AIProfileFormService.js'; -import { AIAdminProfilesResource } from './AIProfilesResource.js'; import AIProfilesToolsPanelStyles from './AIProfilesToolsPanel.module.css'; import { AIProfilesTable } from './AIProfilesTable.js'; import { useAIProfilesTable } from './useAIProfilesTable.js'; -interface Props { - formState: AdministrationAISettingsFormState; -} - const toolsPanelRegistry: StyleRegistry = [ [ToolsPanelStyles, { mode: 'append', styles: [AIProfilesToolsPanelStyles] }], [ToolsActionStyles, { mode: 'append', styles: [AIProfilesToolsPanelStyles] }], ]; -export const AIProfilesPanel = observer(function AIProfilesPanel({ formState }) { +export const AIProfilesPanel = observer(function AIProfilesPanel() { const translate = useTranslate(); const aiProfileFormService = useService(AIProfileFormService); + const aiSettingsService = useService(AISettingsService); + const aiSettingsResource = useService(AISettingsResource); - const settingsInfoPart = getAdministrationAISettingsFormInfoPart(formState); - useAutoLoad(AIProfilesPanel, settingsInfoPart); - - const profilesLoader = useResource(AIProfilesPanel, AIAdminProfilesResource, CachedMapAllKey); + useResource(AIProfilesPanel, AISettingsResource, undefined); + const profilesLoader = useResource(AIProfilesPanel, AIProfilesResource, CachedMapAllKey); const profiles = profilesLoader.data.filter(isDefined); - const defaultProfileId = settingsInfoPart.initialState.defaultConfiguration; + const settingsLoaded = aiSettingsResource.isLoaded(); - const selection = useTableSelection(profiles.filter(p => p.id !== defaultProfileId).map(p => p.id)); + const selection = useTableSelection( + profiles.filter(profile => settingsLoaded && !aiSettingsService.isEffectiveDefaultProfile(profile.id)).map(profile => profile.id), + ); const table = useAIProfilesTable(selection); return ( @@ -95,7 +92,11 @@ export const AIProfilesPanel = observer(function AIProfilesPanel({ formSt - + aiSettingsService.isEffectiveDefaultProfile(profileId)} + /> diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesTable.tsx b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesTable.tsx similarity index 92% rename from webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesTable.tsx rename to webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesTable.tsx index f6ed46a2ba4..c58e9c70052 100644 --- a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesTable.tsx +++ b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesTable.tsx @@ -13,13 +13,15 @@ import { IconOrImage, Link, s, TextPlaceholder, useResource, useS, useTranslate import { useService } from '@cloudbeaver/core-di'; import { ADMINISTRATION_TABLE_DEFAULT_ROW_HEIGHT, AdministrationTableStyles } from '@cloudbeaver/core-administration'; import { DataGrid, TableRowSelect, useCreateGridReactiveValue } from '@cloudbeaver/plugin-data-grid'; -import { AiEnginesResource, type AIProfile } from '@cloudbeaver/plugin-ai'; +import { AiEnginesResource } from '@cloudbeaver/plugin-ai'; +import type { AIProfile } from '@cloudbeaver/plugin-ai-profiles'; import { Command } from '@dbeaver/ui-kit'; import { AIProfileFormService } from './AIProfileForm/AIProfileFormService.js'; interface Props { profiles: AIProfile[]; - defaultProfileId: string | null; + deletionDisabled: boolean; + isDefaultProfile: (profileId: string) => boolean; } const ENGINE_COLUMN_WIDTH = 160; @@ -30,7 +32,7 @@ const ENGINE_COLUMN = { key: 'engine', label: 'plugin_ai_administration_profile_ const COLUMNS = [SELECT_COLUMN, NAME_COLUMN, ENGINE_COLUMN]; -export const AIProfilesTable = observer(function AIProfilesTable({ profiles, defaultProfileId }) { +export const AIProfilesTable = observer(function AIProfilesTable({ profiles, deletionDisabled, isDefaultProfile }) { const translate = useTranslate(); const styles = useS(AdministrationTableStyles); const aiProfileFormService = useService(AIProfileFormService); @@ -51,13 +53,13 @@ export const AIProfilesTable = observer(function AIProfilesTable({ profil return null; } - const isDefault = profile.id === defaultProfileId; + const isDefault = isDefaultProfile(profile.id); if (column.key === SELECT_COLUMN.key) { return ( ); @@ -103,7 +105,8 @@ export const AIProfilesTable = observer(function AIProfilesTable({ profil const cell = useCreateGridReactiveValue(getCell, (onValueChange, rowIdx, colIdx) => reaction(() => getCell(rowIdx, colIdx), onValueChange), [ COLUMNS, profiles, - defaultProfileId, + deletionDisabled, + isDefaultProfile, aiProfileFormService, enginesLoader.data, ]); diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesToolsPanel.module.css b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesToolsPanel.module.css similarity index 100% rename from webapp/packages/plugin-ai-administration/src/AIProfiles/AIProfilesToolsPanel.module.css rename to webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesToolsPanel.module.css diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/useAIProfilesTable.ts b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/useAIProfilesTable.ts similarity index 86% rename from webapp/packages/plugin-ai-administration/src/AIProfiles/useAIProfilesTable.ts rename to webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/useAIProfilesTable.ts index ca2a79eb719..610b28483f3 100644 --- a/webapp/packages/plugin-ai-administration/src/AIProfiles/useAIProfilesTable.ts +++ b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/useAIProfilesTable.ts @@ -13,13 +13,15 @@ import { useService } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; +import { AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; import type { ITableSelection } from '@cloudbeaver/plugin-data-grid'; -import { AIAdminProfilesResource } from './AIProfilesResource.js'; +import { AIProfilesAdministrationService } from './AIProfilesAdministrationService.js'; interface State { processing: boolean; - aiProfilesResource: AIAdminProfilesResource; + aiProfilesResource: AIProfilesResource; + aiProfilesAdministrationService: AIProfilesAdministrationService; notificationService: NotificationService; dialogService: CommonDialogService; selection: ITableSelection; @@ -30,7 +32,8 @@ interface State { export function useAIProfilesTable(selection: ITableSelection): Readonly { const notificationService = useService(NotificationService); const dialogService = useService(CommonDialogService); - const aiProfilesResource = useService(AIAdminProfilesResource); + const aiProfilesResource = useService(AIProfilesResource); + const aiProfilesAdministrationService = useService(AIProfilesAdministrationService); const translate = useTranslate(); return useObservableRef( @@ -81,7 +84,7 @@ export function useAIProfilesTable(selection: ITableSelection): Readonly try { this.processing = true; - const results = await Promise.allSettled(deletionList.map(profileId => this.aiProfilesResource.deleteProfile(profileId))); + const results = await Promise.allSettled(deletionList.map(profileId => this.aiProfilesAdministrationService.delete(profileId))); const failed = results.filter((r): r is PromiseRejectedResult => r.status === 'rejected'); if (failed.length === 0) { @@ -105,6 +108,6 @@ export function useAIProfilesTable(selection: ITableSelection): Readonly refresh: action.bound, delete: action.bound, }, - { aiProfilesResource, selection, notificationService, dialogService }, + { aiProfilesResource, aiProfilesAdministrationService, selection, notificationService, dialogService }, ); } diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/utils/getObjectPropertiesValues.ts b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/utils/getObjectPropertiesValues.ts similarity index 100% rename from webapp/packages/plugin-ai-administration/src/AIProfiles/utils/getObjectPropertiesValues.ts rename to webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/utils/getObjectPropertiesValues.ts diff --git a/webapp/packages/plugin-ai-administration/src/AIProfiles/utils/prepareProperties.ts b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/utils/prepareProperties.ts similarity index 100% rename from webapp/packages/plugin-ai-administration/src/AIProfiles/utils/prepareProperties.ts rename to webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/utils/prepareProperties.ts diff --git a/webapp/packages/plugin-ai-profiles-administration/src/AIProfilesAdministrationBootstrap.ts b/webapp/packages/plugin-ai-profiles-administration/src/AIProfilesAdministrationBootstrap.ts new file mode 100644 index 00000000000..80db2e81406 --- /dev/null +++ b/webapp/packages/plugin-ai-profiles-administration/src/AIProfilesAdministrationBootstrap.ts @@ -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 { AIAdministrationBootstrap, AIAdministrationTabsService, EAIAdministrationSub } from '@cloudbeaver/plugin-ai-administration'; + +const AIProfilesTabPanel = importLazyComponent(() => import('./AIProfilesTabPanel.js').then(module => module.AIProfilesTabPanel)); + +@injectable(() => [AIAdministrationBootstrap, AIAdministrationTabsService]) +export class AIProfilesAdministrationBootstrap extends Bootstrap { + constructor( + private readonly aiAdministrationBootstrap: AIAdministrationBootstrap, + private readonly aiAdministrationTabsService: AIAdministrationTabsService, + ) { + super(); + } + + override register(): void { + this.aiAdministrationTabsService.tabsContainer.add({ + key: EAIAdministrationSub.Profiles, + name: 'plugin_ai_administration_profiles_title', + order: 2, + panel: () => AIProfilesTabPanel, + }); + this.aiAdministrationBootstrap.administrationItem.sub.push({ name: EAIAdministrationSub.Profiles }); + } +} diff --git a/webapp/packages/plugin-ai-profiles-administration/src/AIProfilesTabPanel.tsx b/webapp/packages/plugin-ai-profiles-administration/src/AIProfilesTabPanel.tsx new file mode 100644 index 00000000000..1a184450bd5 --- /dev/null +++ b/webapp/packages/plugin-ai-profiles-administration/src/AIProfilesTabPanel.tsx @@ -0,0 +1,14 @@ +/* + * 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 { AIProfilesPanel } from './AIProfiles/AIProfilesPanel.js'; + +export const AIProfilesTabPanel = observer(function AIProfilesTabPanel() { + return ; +}); diff --git a/webapp/packages/plugin-ai-profiles-administration/src/LocaleService.ts b/webapp/packages/plugin-ai-profiles-administration/src/LocaleService.ts new file mode 100644 index 00000000000..a5cddc83012 --- /dev/null +++ b/webapp/packages/plugin-ai-profiles-administration/src/LocaleService.ts @@ -0,0 +1,34 @@ +/* + * 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 { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { LocalizationService } from '@cloudbeaver/core-localization'; + +@injectable(() => [LocalizationService]) +export class LocaleService extends Bootstrap { + constructor(private readonly localizationService: LocalizationService) { + super(); + } + + override register(): void { + this.localizationService.addProvider(this.provider.bind(this)); + } + + private async provider(locale: string) { + switch (locale) { + case 'ru': + return (await import('./locales/ru.js')).default; + case 'de': + return (await import('./locales/de.js')).default; + case 'fr': + return (await import('./locales/fr.js')).default; + default: + return (await import('./locales/en.js')).default; + } + } +} diff --git a/webapp/packages/plugin-ai-profiles-administration/src/index.ts b/webapp/packages/plugin-ai-profiles-administration/src/index.ts new file mode 100644 index 00000000000..389b81c183e --- /dev/null +++ b/webapp/packages/plugin-ai-profiles-administration/src/index.ts @@ -0,0 +1,11 @@ +/* + * 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 './module.js'; + +export { AIProfilesAdministrationBootstrap } from './AIProfilesAdministrationBootstrap.js'; diff --git a/webapp/packages/plugin-ai-profiles-administration/src/locales/de.ts b/webapp/packages/plugin-ai-profiles-administration/src/locales/de.ts new file mode 100644 index 00000000000..d2e8e1d0a0d --- /dev/null +++ b/webapp/packages/plugin-ai-profiles-administration/src/locales/de.ts @@ -0,0 +1,40 @@ +export default [ + ['ai_administration_language_model_settings', 'Modelleinstellungen'], + ['ai_administration_select_language_model_selector_title', 'Modell'], + ['ai_administration_models_refresh', 'Modelle aktualisieren'], + ['ai_administration_models_refresh_description', 'Pflichtfelder ausfüllen und dann aktualisieren, um Modelle zu laden'], + ['ai_administration_models_refresh_fail', 'Modelle konnten nicht aktualisiert werden'], + ['plugin_ai_administration_profiles_title', 'Profile'], + ['plugin_ai_administration_profiles_table_empty_placeholder', 'Keine Profile gefunden. Erstellen Sie ein neues Profil'], + ['plugin_ai_administration_profile_column_name', 'Name'], + ['plugin_ai_administration_profile_column_engine', 'Engine'], + ['plugin_ai_administration_profile_default_badge', '(Standard)'], + ['plugin_ai_administration_profile_add_tooltip', 'Neues Profil erstellen'], + ['plugin_ai_administration_profile_refresh_tooltip', 'Profilliste aktualisieren'], + ['plugin_ai_administration_profile_delete_tooltip', 'Ausgewählte Profile löschen'], + ['plugin_ai_administration_profiles_refresh_success', 'Profilliste aktualisiert'], + ['plugin_ai_administration_profiles_refresh_error', 'Profilliste konnte nicht aktualisiert werden'], + ['plugin_ai_administration_profile_delete_confirmation', 'Sie sind dabei, folgende Profile zu löschen: '], + ['plugin_ai_administration_profile_delete_user_credentials_warning', 'Gespeicherte Anmeldedaten für alle Benutzer dieser Profile werden ebenfalls gelöscht.'], + ['plugin_ai_administration_profile_delete_success', 'Ausgewählte Profile gelöscht'], + ['plugin_ai_administration_profile_delete_error', 'Profile konnten nicht gelöscht werden'], + ['plugin_ai_administration_profile_created', 'Profil erstellt'], + ['plugin_ai_administration_profile_updated', 'Profil aktualisiert'], + ['plugin_ai_administration_profile_create_error', 'Profil konnte nicht erstellt werden'], + ['plugin_ai_administration_profile_save_error', 'Profil konnte nicht gespeichert werden'], + ['plugin_ai_administration_profile_form_field_name', 'Profilname'], + ['plugin_ai_administration_profile_form_field_engine', 'Engine'], + ['plugin_ai_administration_profile_profile_type', 'Quelle der Anmeldedaten'], + ['plugin_ai_administration_profile_global_credentials', 'Globale Anmeldedaten'], + ['plugin_ai_administration_profile_user_credentials', 'Benutzeranmeldedaten'], + ['plugin_ai_administration_profile_user_credentials_unsupported', 'Diese Engine unterstützt keine vom Benutzer bereitgestellten API-Token'], + ['plugin_ai_administration_profile_form_tab_options', 'Profil'], + ['plugin_ai_administration_profile_name_max_length', 'Der Profilname darf {arg:length} Zeichen nicht überschreiten'], + ['plugin_ai_administration_profile_name_min_length', 'Der Profilname muss mindestens {arg:length} Zeichen lang sein'], + [ + 'plugin_ai_administration_profile_default_delete_info', + 'Das Standardprofil kann nicht gelöscht werden. Bitte wählen Sie ein anderes Profil als Standard aus, bevor Sie dieses Profil löschen.', + ], + ['plugin_ai_administration_profile_create', 'Neues Profil'], + ['plugin_ai_administration_profile_edit', 'Profil bearbeiten'], +]; diff --git a/webapp/packages/plugin-ai-profiles-administration/src/locales/en.ts b/webapp/packages/plugin-ai-profiles-administration/src/locales/en.ts new file mode 100644 index 00000000000..868cd450801 --- /dev/null +++ b/webapp/packages/plugin-ai-profiles-administration/src/locales/en.ts @@ -0,0 +1,40 @@ +export default [ + ['ai_administration_language_model_settings', 'Model settings'], + ['ai_administration_select_language_model_selector_title', 'Model'], + ['ai_administration_models_refresh', 'Refresh models'], + ['ai_administration_models_refresh_description', 'Fill in required fields, then refresh to load models'], + ['ai_administration_models_refresh_fail', 'Failed to refresh models'], + ['plugin_ai_administration_profiles_title', 'Profiles'], + ['plugin_ai_administration_profiles_table_empty_placeholder', 'No profiles found. Create a new profile'], + ['plugin_ai_administration_profile_column_name', 'Name'], + ['plugin_ai_administration_profile_column_engine', 'Engine'], + ['plugin_ai_administration_profile_default_badge', '(Default)'], + ['plugin_ai_administration_profile_add_tooltip', 'Create new profile'], + ['plugin_ai_administration_profile_refresh_tooltip', 'Refresh profiles list'], + ['plugin_ai_administration_profile_delete_tooltip', 'Delete selected profiles'], + ['plugin_ai_administration_profiles_refresh_success', 'Profiles list updated'], + ['plugin_ai_administration_profiles_refresh_error', 'Failed to refresh profiles list'], + ['plugin_ai_administration_profile_delete_confirmation', 'You are about to delete profile(s): '], + ['plugin_ai_administration_profile_delete_user_credentials_warning', 'Saved credentials for all users of these profiles will also be deleted.'], + ['plugin_ai_administration_profile_delete_success', 'Selected profiles deleted'], + ['plugin_ai_administration_profile_delete_error', 'Failed to delete profiles'], + ['plugin_ai_administration_profile_created', 'Profile created'], + ['plugin_ai_administration_profile_updated', 'Profile updated'], + ['plugin_ai_administration_profile_create_error', 'Failed to create profile'], + ['plugin_ai_administration_profile_save_error', 'Failed to save profile'], + ['plugin_ai_administration_profile_form_field_name', 'Profile name'], + ['plugin_ai_administration_profile_form_field_engine', 'Engine'], + ['plugin_ai_administration_profile_profile_type', 'Credential source'], + ['plugin_ai_administration_profile_global_credentials', 'Global credentials'], + ['plugin_ai_administration_profile_user_credentials', 'User credentials'], + ['plugin_ai_administration_profile_user_credentials_unsupported', 'This engine does not support user-provided API tokens'], + ['plugin_ai_administration_profile_form_tab_options', 'Profile'], + ['plugin_ai_administration_profile_name_max_length', 'Profile name must not exceed {arg:length} characters'], + ['plugin_ai_administration_profile_name_min_length', 'Profile name must be at least {arg:length} characters'], + [ + 'plugin_ai_administration_profile_default_delete_info', + 'The default profile cannot be deleted. Please select another profile as the default before deleting this profile.', + ], + ['plugin_ai_administration_profile_create', 'New Profile'], + ['plugin_ai_administration_profile_edit', 'Edit Profile'], +]; diff --git a/webapp/packages/plugin-ai-profiles-administration/src/locales/fr.ts b/webapp/packages/plugin-ai-profiles-administration/src/locales/fr.ts new file mode 100644 index 00000000000..4c85a67745f --- /dev/null +++ b/webapp/packages/plugin-ai-profiles-administration/src/locales/fr.ts @@ -0,0 +1,40 @@ +export default [ + ['ai_administration_language_model_settings', 'Paramètres du modèle'], + ['ai_administration_select_language_model_selector_title', 'Modèle'], + ['ai_administration_models_refresh', 'Actualiser les modèles'], + ['ai_administration_models_refresh_description', 'Remplissez les champs requis, puis actualisez pour charger les modèles'], + ['ai_administration_models_refresh_fail', "Échec de l'actualisation des modèles"], + ['plugin_ai_administration_profiles_title', 'Profils'], + ['plugin_ai_administration_profiles_table_empty_placeholder', 'Aucun profil trouvé. Créez un nouveau profil'], + ['plugin_ai_administration_profile_column_name', 'Nom'], + ['plugin_ai_administration_profile_column_engine', "Modèle d'IA"], + ['plugin_ai_administration_profile_default_badge', '(Par défaut)'], + ['plugin_ai_administration_profile_add_tooltip', 'Créer un nouveau profil'], + ['plugin_ai_administration_profile_refresh_tooltip', 'Actualiser la liste des profils'], + ['plugin_ai_administration_profile_delete_tooltip', 'Supprimer les profils sélectionnés'], + ['plugin_ai_administration_profiles_refresh_success', 'Liste des profils mise à jour'], + ['plugin_ai_administration_profiles_refresh_error', 'Échec de l’actualisation de la liste des profils'], + ['plugin_ai_administration_profile_delete_confirmation', 'Vous êtes sur le point de supprimer le(s) profil(s) : '], + ['plugin_ai_administration_profile_delete_user_credentials_warning', 'Les identifiants enregistrés pour tous les utilisateurs de ces profils seront également supprimés.'], + ['plugin_ai_administration_profile_delete_success', 'Profils sélectionnés supprimés'], + ['plugin_ai_administration_profile_delete_error', 'Échec de la suppression des profils'], + ['plugin_ai_administration_profile_created', 'Profil créé'], + ['plugin_ai_administration_profile_updated', 'Profil mis à jour'], + ['plugin_ai_administration_profile_create_error', 'Échec de la création du profil'], + ['plugin_ai_administration_profile_save_error', 'Échec de l’enregistrement du profil'], + ['plugin_ai_administration_profile_form_field_name', 'Nom du profil'], + ['plugin_ai_administration_profile_form_field_engine', "Modèle d'IA"], + ['plugin_ai_administration_profile_profile_type', 'Source des identifiants'], + ['plugin_ai_administration_profile_global_credentials', 'Identifiants globaux'], + ['plugin_ai_administration_profile_user_credentials', 'Identifiants utilisateur'], + ['plugin_ai_administration_profile_user_credentials_unsupported', 'Ce moteur ne prend pas en charge les jetons API fournis par les utilisateurs'], + ['plugin_ai_administration_profile_form_tab_options', 'Profil'], + ['plugin_ai_administration_profile_name_max_length', 'Le nom du profil ne doit pas dépasser {arg:length} caractères'], + ['plugin_ai_administration_profile_name_min_length', 'Le nom du profil doit contenir au moins {arg:length} caractères'], + [ + 'plugin_ai_administration_profile_default_delete_info', + 'Le profil par défaut ne peut pas être supprimé. Veuillez sélectionner un autre profil comme profil par défaut avant de supprimer ce profil.', + ], + ['plugin_ai_administration_profile_create', 'Nouveau profil'], + ['plugin_ai_administration_profile_edit', 'Modifier le profil'], +]; diff --git a/webapp/packages/plugin-ai-profiles-administration/src/locales/ru.ts b/webapp/packages/plugin-ai-profiles-administration/src/locales/ru.ts new file mode 100644 index 00000000000..f48383d8a17 --- /dev/null +++ b/webapp/packages/plugin-ai-profiles-administration/src/locales/ru.ts @@ -0,0 +1,40 @@ +export default [ + ['ai_administration_language_model_settings', 'Настройки модели'], + ['ai_administration_select_language_model_selector_title', 'Модель'], + ['ai_administration_models_refresh', 'Обновить модели'], + ['ai_administration_models_refresh_description', 'Заполните обязательные поля и обновите список моделей'], + ['ai_administration_models_refresh_fail', 'Не удалось обновить модели'], + ['plugin_ai_administration_profiles_title', 'Профили'], + ['plugin_ai_administration_profiles_table_empty_placeholder', 'Профили не найдены. Создайте новый профиль'], + ['plugin_ai_administration_profile_column_name', 'Название'], + ['plugin_ai_administration_profile_column_engine', 'Энджин'], + ['plugin_ai_administration_profile_default_badge', '(По умолчанию)'], + ['plugin_ai_administration_profile_add_tooltip', 'Создать новый профиль'], + ['plugin_ai_administration_profile_refresh_tooltip', 'Обновить список профилей'], + ['plugin_ai_administration_profile_delete_tooltip', 'Удалить выбранные профили'], + ['plugin_ai_administration_profiles_refresh_success', 'Список профилей обновлён'], + ['plugin_ai_administration_profiles_refresh_error', 'Не удалось обновить список профилей'], + ['plugin_ai_administration_profile_delete_confirmation', 'Вы собираетесь удалить профили(ь): '], + ['plugin_ai_administration_profile_delete_user_credentials_warning', 'Сохраненные учетные данные всех пользователей этих профилей также будут удалены.'], + ['plugin_ai_administration_profile_delete_success', 'Выбранные профили удалены'], + ['plugin_ai_administration_profile_delete_error', 'Не удалось удалить профили'], + ['plugin_ai_administration_profile_created', 'Профиль создан'], + ['plugin_ai_administration_profile_updated', 'Профиль обновлён'], + ['plugin_ai_administration_profile_create_error', 'Не удалось создать профиль'], + ['plugin_ai_administration_profile_save_error', 'Не удалось сохранить профиль'], + ['plugin_ai_administration_profile_form_field_name', 'Название профиля'], + ['plugin_ai_administration_profile_form_field_engine', 'Энджин'], + ['plugin_ai_administration_profile_profile_type', 'Источник учетных данных'], + ['plugin_ai_administration_profile_global_credentials', 'Глобальные учетные данные'], + ['plugin_ai_administration_profile_user_credentials', 'Учетные данные пользователя'], + ['plugin_ai_administration_profile_user_credentials_unsupported', 'Этот движок не поддерживает API-токены, предоставляемые пользователями'], + ['plugin_ai_administration_profile_form_tab_options', 'Профиль'], + ['plugin_ai_administration_profile_name_max_length', 'Название профиля не должно превышать {arg:length} символов'], + ['plugin_ai_administration_profile_name_min_length', 'Название профиля должно содержать не менее {arg:length} символов'], + [ + 'plugin_ai_administration_profile_default_delete_info', + 'Профиль по умолчанию нельзя удалить. Перед удалением этого профиля выберите другой профиль по умолчанию.', + ], + ['plugin_ai_administration_profile_create', 'Новый профиль'], + ['plugin_ai_administration_profile_edit', 'Редактировать профиль'], +]; diff --git a/webapp/packages/plugin-ai-profiles-administration/src/module.ts b/webapp/packages/plugin-ai-profiles-administration/src/module.ts new file mode 100644 index 00000000000..5d0c4071ba0 --- /dev/null +++ b/webapp/packages/plugin-ai-profiles-administration/src/module.ts @@ -0,0 +1,32 @@ +/* + * 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 { Bootstrap, Dependency, ModuleRegistry, proxy } from '@cloudbeaver/core-di'; + +import { AIEnginePropertiesResource } from './AIProfiles/AIEnginePropertiesResource.js'; +import { AIProfileFormService } from './AIProfiles/AIProfileForm/AIProfileFormService.js'; +import { AIProfileFormTabBootstrap } from './AIProfiles/AIProfileForm/AIProfileFormTabBootstrap.js'; +import { AIProfilesAdministrationService } from './AIProfiles/AIProfilesAdministrationService.js'; +import { AIProfilesAdministrationBootstrap } from './AIProfilesAdministrationBootstrap.js'; +import { LocaleService } from './LocaleService.js'; + +export default ModuleRegistry.add({ + name: '@cloudbeaver/plugin-ai-profiles-administration', + + configure: serviceCollection => { + serviceCollection + .addSingleton(Bootstrap, proxy(AIProfilesAdministrationBootstrap)) + .addSingleton(Bootstrap, AIProfileFormTabBootstrap) + .addSingleton(Bootstrap, LocaleService) + .addSingleton(Dependency, proxy(AIEnginePropertiesResource)) + .addSingleton(AIProfilesAdministrationBootstrap) + .addSingleton(AIProfilesAdministrationService) + .addSingleton(AIEnginePropertiesResource) + .addSingleton(AIProfileFormService); + }, +}); diff --git a/webapp/packages/plugin-ai-profiles-administration/tsconfig.json b/webapp/packages/plugin-ai-profiles-administration/tsconfig.json new file mode 100644 index 00000000000..f73f5ae9f62 --- /dev/null +++ b/webapp/packages/plugin-ai-profiles-administration/tsconfig.json @@ -0,0 +1,81 @@ +{ + "extends": "@cloudbeaver/tsconfig/tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true + }, + "references": [ + { + "path": "../../common-react/@dbeaver/ui-kit" + }, + { + "path": "../../common-typescript/@dbeaver/js-helpers" + }, + { + "path": "../core-administration" + }, + { + "path": "../core-blocks" + }, + { + "path": "../core-cli" + }, + { + "path": "../core-data-context" + }, + { + "path": "../core-di" + }, + { + "path": "../core-dialogs" + }, + { + "path": "../core-events" + }, + { + "path": "../core-executor" + }, + { + "path": "../core-localization" + }, + { + "path": "../core-resource" + }, + { + "path": "../core-root" + }, + { + "path": "../core-sdk" + }, + { + "path": "../core-ui" + }, + { + "path": "../core-utils" + }, + { + "path": "../plugin-ai" + }, + { + "path": "../plugin-ai-administration" + }, + { + "path": "../plugin-ai-profiles" + }, + { + "path": "../plugin-data-grid" + } + ], + "include": [ + "__custom_mocks__/**/*", + "src/**/*", + "src/**/*.json", + "src/**/*.css" + ], + "exclude": [ + "**/node_modules", + "lib/**/*" + ] +} diff --git a/webapp/packages/plugin-ai-profiles/.gitignore b/webapp/packages/plugin-ai-profiles/.gitignore new file mode 100644 index 00000000000..12c18d4eded --- /dev/null +++ b/webapp/packages/plugin-ai-profiles/.gitignore @@ -0,0 +1 @@ +/lib/ diff --git a/webapp/packages/plugin-ai-profiles/package.json b/webapp/packages/plugin-ai-profiles/package.json new file mode 100644 index 00000000000..2bf193f91d7 --- /dev/null +++ b/webapp/packages/plugin-ai-profiles/package.json @@ -0,0 +1,47 @@ +{ + "name": "@cloudbeaver/plugin-ai-profiles", + "type": "module", + "sideEffects": [ + "./lib/module.js", + "./lib/index.js", + "src/**/*.css", + "public/**/*" + ], + "version": "0.1.0", + "description": "", + "license": "Apache-2.0", + "exports": { + ".": "./lib/index.js", + "./module": "./lib/module.js" + }, + "scripts": { + "build": "tsc -b", + "clean": "rimraf --glob lib", + "lint": "eslint ./src/ --ext .ts,.tsx", + "validate-dependencies": "core-cli-validate-dependencies" + }, + "dependencies": { + "@cloudbeaver/core-authentication": "workspace:*", + "@cloudbeaver/core-blocks": "workspace:*", + "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-dialogs": "workspace:*", + "@cloudbeaver/core-events": "workspace:*", + "@cloudbeaver/core-localization": "workspace:*", + "@cloudbeaver/core-resource": "workspace:*", + "@cloudbeaver/core-root": "workspace:*", + "@cloudbeaver/core-sdk": "workspace:*", + "@cloudbeaver/plugin-ai": "workspace:*", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^19", + "react-dom": "^19", + "tslib": "^2" + }, + "devDependencies": { + "@cloudbeaver/core-cli": "workspace:*", + "@cloudbeaver/tsconfig": "workspace:*", + "@types/react": "^19", + "rimraf": "^6", + "typescript": "^5" + } +} diff --git a/webapp/packages/plugin-ai/src/AIProfileCredentialsDialog.tsx b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx similarity index 91% rename from webapp/packages/plugin-ai/src/AIProfileCredentialsDialog.tsx rename to webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx index 3d472142704..c8dd2ea599d 100644 --- a/webapp/packages/plugin-ai/src/AIProfileCredentialsDialog.tsx +++ b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx @@ -28,7 +28,7 @@ import { CommonDialogService, DialogueStateResult, type DialogComponent } from ' import { NotificationService } from '@cloudbeaver/core-events'; import type { IAIProfileCredentialsDialogPayload } from './IAIProfileCredentialsDialogPayload.js'; -import { UserAIProfileResource } from './UserAIProfileResource.js'; +import { AIProfilesResource } from './AIProfilesResource.js'; interface CredentialsDialogState { token: string; @@ -36,7 +36,6 @@ interface CredentialsDialogState { credentialsSaved: boolean; } -// TODO: Move this UI to a dedicated shared AI credentials UI package when package boundaries warrant it. export const AIProfileCredentialsDialog: DialogComponent = observer(function AIProfileCredentialsDialog({ payload, resolveDialog, @@ -45,7 +44,7 @@ export const AIProfileCredentialsDialog: DialogComponent( () => ({ token: '', processing: false, credentialsSaved: payload.credentialsSaved }), { token: observable.ref, processing: observable.ref, credentialsSaved: observable.ref }, @@ -56,7 +55,7 @@ export const AIProfileCredentialsDialog: DialogComponent [CommonDialogService, NotificationService, UserAIProfileResource, AiEnginesResource]) +@injectable(() => [CommonDialogService, NotificationService, AIProfilesResource, AiEnginesResource]) export class AIProfileCredentialsService { constructor( private readonly commonDialogService: CommonDialogService, private readonly notificationService: NotificationService, - private readonly userAIProfileResource: UserAIProfileResource, + private readonly aiProfilesResource: AIProfilesResource, private readonly aiEnginesResource: AiEnginesResource, ) {} async open(profileId: string): Promise> { - const [profile] = await Promise.all([this.userAIProfileResource.load(profileId), this.aiEnginesResource.load()]); + const [profile] = await Promise.all([this.aiProfilesResource.load(profileId), this.aiEnginesResource.load()]); if (!profile) { this.notificationService.logError({ title: 'plugin_ai_credentials_profile_not_found' }); @@ -40,6 +41,12 @@ export class AIProfileCredentialsService { credentialsSaved: profile.credentialsSaved, }); } -} -export { AIProfileCredentialsService as AIProfileCredentialsDialogService }; + isSupported(properties: ReadonlyArray<{ id?: string; features: readonly string[] }>): boolean { + return supportsUserCredentials(properties); + } + + isRequired(profile: Pick): boolean { + return requiresUserCredentials(profile); + } +} diff --git a/webapp/packages/plugin-ai/src/AIProfileCredentialsUtils.ts b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsUtils.ts similarity index 65% rename from webapp/packages/plugin-ai/src/AIProfileCredentialsUtils.ts rename to webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsUtils.ts index 01f3248e2b5..2863a65ea1d 100644 --- a/webapp/packages/plugin-ai/src/AIProfileCredentialsUtils.ts +++ b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsUtils.ts @@ -6,16 +6,12 @@ * you may not use this file except in compliance with the License. */ -import type { AIProfile } from './UserAIProfileResource.js'; +import type { AIProfile } from './AIProfilesResource.js'; export function supportsUserCredentials(properties: ReadonlyArray<{ id?: string; features: readonly string[] }>): boolean { return properties.some(property => property.id === 'token' && property.features.includes('password')); } -export function requireGlobalProfileToken(properties: readonly T[], global: boolean): T[] { - return properties.map(property => (global && property.id === 'token' ? { ...property, required: true } : property)); -} - export function requiresUserCredentials(profile: Pick): boolean { return !profile.global && !profile.credentialsSaved; } diff --git a/webapp/packages/plugin-ai/src/UserAIProfileResource.ts b/webapp/packages/plugin-ai-profiles/src/AIProfilesResource.ts similarity index 97% rename from webapp/packages/plugin-ai/src/UserAIProfileResource.ts rename to webapp/packages/plugin-ai-profiles/src/AIProfilesResource.ts index da6829d20cf..3bfdd220417 100644 --- a/webapp/packages/plugin-ai/src/UserAIProfileResource.ts +++ b/webapp/packages/plugin-ai-profiles/src/AIProfilesResource.ts @@ -15,7 +15,7 @@ import { type AiConfigurationProfileInfo, GraphQLService } from '@cloudbeaver/co export type AIProfile = AiConfigurationProfileInfo; @injectable(() => [GraphQLService, ServerConfigResource, WorkspaceConfigEventHandler, UserInfoResource]) -export class UserAIProfileResource extends CachedMapResource { +export class AIProfilesResource extends CachedMapResource { constructor( private readonly graphQLService: GraphQLService, serverConfigResource: ServerConfigResource, diff --git a/webapp/packages/plugin-ai/src/IAIProfileCredentialsDialogPayload.ts b/webapp/packages/plugin-ai-profiles/src/IAIProfileCredentialsDialogPayload.ts similarity index 100% rename from webapp/packages/plugin-ai/src/IAIProfileCredentialsDialogPayload.ts rename to webapp/packages/plugin-ai-profiles/src/IAIProfileCredentialsDialogPayload.ts diff --git a/webapp/packages/plugin-ai/src/LocaleService.ts b/webapp/packages/plugin-ai-profiles/src/LocaleService.ts similarity index 100% rename from webapp/packages/plugin-ai/src/LocaleService.ts rename to webapp/packages/plugin-ai-profiles/src/LocaleService.ts diff --git a/webapp/packages/plugin-ai-profiles/src/index.ts b/webapp/packages/plugin-ai-profiles/src/index.ts new file mode 100644 index 00000000000..b865a36928d --- /dev/null +++ b/webapp/packages/plugin-ai-profiles/src/index.ts @@ -0,0 +1,14 @@ +/* + * 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 './module.js'; + +export * from './AIProfileCredentialsDialogLazy.js'; +export * from './AIProfileCredentialsService.js'; +export * from './AIProfilesResource.js'; +export * from './IAIProfileCredentialsDialogPayload.js'; diff --git a/webapp/packages/plugin-ai/src/locales/en.ts b/webapp/packages/plugin-ai-profiles/src/locales/en.ts similarity index 100% rename from webapp/packages/plugin-ai/src/locales/en.ts rename to webapp/packages/plugin-ai-profiles/src/locales/en.ts diff --git a/webapp/packages/plugin-ai/src/locales/fr.ts b/webapp/packages/plugin-ai-profiles/src/locales/fr.ts similarity index 100% rename from webapp/packages/plugin-ai/src/locales/fr.ts rename to webapp/packages/plugin-ai-profiles/src/locales/fr.ts diff --git a/webapp/packages/plugin-ai/src/locales/ru.ts b/webapp/packages/plugin-ai-profiles/src/locales/ru.ts similarity index 100% rename from webapp/packages/plugin-ai/src/locales/ru.ts rename to webapp/packages/plugin-ai-profiles/src/locales/ru.ts diff --git a/webapp/packages/plugin-ai/src/locales/zh.ts b/webapp/packages/plugin-ai-profiles/src/locales/zh.ts similarity index 100% rename from webapp/packages/plugin-ai/src/locales/zh.ts rename to webapp/packages/plugin-ai-profiles/src/locales/zh.ts diff --git a/webapp/packages/plugin-ai-profiles/src/module.ts b/webapp/packages/plugin-ai-profiles/src/module.ts new file mode 100644 index 00000000000..f19b6a35c56 --- /dev/null +++ b/webapp/packages/plugin-ai-profiles/src/module.ts @@ -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 { Bootstrap, Dependency, ModuleRegistry, proxy } from '@cloudbeaver/core-di'; + +import { AIProfileCredentialsService } from './AIProfileCredentialsService.js'; +import { AIProfilesResource } from './AIProfilesResource.js'; +import { LocaleService } from './LocaleService.js'; + +export default ModuleRegistry.add({ + name: '@cloudbeaver/plugin-ai-profiles', + + configure: serviceCollection => { + serviceCollection + .addSingleton(Bootstrap, LocaleService) + .addSingleton(Dependency, proxy(AIProfilesResource)) + .addSingleton(AIProfilesResource) + .addSingleton(AIProfileCredentialsService); + }, +}); diff --git a/webapp/packages/plugin-ai-profiles/tsconfig.json b/webapp/packages/plugin-ai-profiles/tsconfig.json new file mode 100644 index 00000000000..e16ea68242d --- /dev/null +++ b/webapp/packages/plugin-ai-profiles/tsconfig.json @@ -0,0 +1,54 @@ +{ + "extends": "@cloudbeaver/tsconfig/tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + "tsBuildInfoFile": "lib/tsconfig.tsbuildinfo", + "composite": true + }, + "references": [ + { + "path": "../core-authentication" + }, + { + "path": "../core-blocks" + }, + { + "path": "../core-cli" + }, + { + "path": "../core-di" + }, + { + "path": "../core-dialogs" + }, + { + "path": "../core-events" + }, + { + "path": "../core-localization" + }, + { + "path": "../core-resource" + }, + { + "path": "../core-root" + }, + { + "path": "../core-sdk" + }, + { + "path": "../plugin-ai" + } + ], + "include": [ + "__custom_mocks__/**/*", + "src/**/*", + "src/**/*.json", + "src/**/*.css" + ], + "exclude": [ + "**/node_modules", + "lib/**/*" + ] +} diff --git a/webapp/packages/plugin-ai-user-profile/package.json b/webapp/packages/plugin-ai-user-profile/package.json index ef88bf300ce..cb621a0c294 100644 --- a/webapp/packages/plugin-ai-user-profile/package.json +++ b/webapp/packages/plugin-ai-user-profile/package.json @@ -29,6 +29,7 @@ "@cloudbeaver/core-resource": "workspace:*", "@cloudbeaver/core-root": "workspace:*", "@cloudbeaver/plugin-ai": "workspace:*", + "@cloudbeaver/plugin-ai-profiles": "workspace:*", "@cloudbeaver/plugin-user-profile": "workspace:*", "mobx": "^6", "mobx-react-lite": "^4", diff --git a/webapp/packages/plugin-ai-user-profile/src/AIUserProfileBootstrap.ts b/webapp/packages/plugin-ai-user-profile/src/AIUserProfileBootstrap.ts index 777b5ca123a..98ded1ac7f2 100644 --- a/webapp/packages/plugin-ai-user-profile/src/AIUserProfileBootstrap.ts +++ b/webapp/packages/plugin-ai-user-profile/src/AIUserProfileBootstrap.ts @@ -10,20 +10,20 @@ import { importLazyComponent } from '@cloudbeaver/core-blocks'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { CachedMapAllKey, getCachedMapResourceLoaderState } from '@cloudbeaver/core-resource'; import { FEATURE_AI_ID, ServerConfigResource } from '@cloudbeaver/core-root'; -import { UserAIProfileResource } from '@cloudbeaver/plugin-ai'; +import { AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; import { UserProfileTabsService } from '@cloudbeaver/plugin-user-profile'; const AIProfilesPanel = importLazyComponent(() => import('./components/AIProfilesPanel.js').then(module => module.AIProfilesPanel)); const AI_PROFILES_TAB_ID = 'ai_profiles'; -@injectable(() => [UserProfileTabsService, AppAuthService, ServerConfigResource, UserAIProfileResource]) +@injectable(() => [UserProfileTabsService, AppAuthService, ServerConfigResource, AIProfilesResource]) export class AIUserProfileBootstrap extends Bootstrap { constructor( private readonly userProfileTabsService: UserProfileTabsService, private readonly appAuthService: AppAuthService, private readonly serverConfigResource: ServerConfigResource, - private readonly userAIProfileResource: UserAIProfileResource, + private readonly aiProfilesResource: AIProfilesResource, ) { super(); } @@ -34,7 +34,7 @@ export class AIUserProfileBootstrap extends Bootstrap { name: 'plugin_ai_user_profile_tab_label', order: 4, getLoader: () => - getCachedMapResourceLoaderState(this.userAIProfileResource, () => + getCachedMapResourceLoaderState(this.aiProfilesResource, () => this.appAuthService.authenticated && this.serverConfigResource.isFeatureEnabled(FEATURE_AI_ID, true) ? CachedMapAllKey : null, ), isHidden: () => !this.isAvailable(), @@ -46,7 +46,7 @@ export class AIUserProfileBootstrap extends Bootstrap { return ( this.appAuthService.authenticated && this.serverConfigResource.isFeatureEnabled(FEATURE_AI_ID, true) && - (!this.userAIProfileResource.isLoaded(CachedMapAllKey) || this.userAIProfileResource.values.length > 0) + (!this.aiProfilesResource.isLoaded(CachedMapAllKey) || this.aiProfilesResource.values.length > 0) ); } } diff --git a/webapp/packages/plugin-ai-user-profile/src/components/AIProfilesPanel.tsx b/webapp/packages/plugin-ai-user-profile/src/components/AIProfilesPanel.tsx index aa48d985bdc..aa9c0332f11 100644 --- a/webapp/packages/plugin-ai-user-profile/src/components/AIProfilesPanel.tsx +++ b/webapp/packages/plugin-ai-user-profile/src/components/AIProfilesPanel.tsx @@ -11,14 +11,15 @@ import { ColoredContainer, Container, Group, TextPlaceholder, ToolsAction, Tools import { CachedMapAllKey } from '@cloudbeaver/core-resource'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; -import { AiEnginesResource, UserAIProfileResource } from '@cloudbeaver/plugin-ai'; +import { AiEnginesResource } from '@cloudbeaver/plugin-ai'; +import { AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; import { AIProfilesTable, type IAIProfile } from './AIProfilesTable.js'; export const AIProfilesPanel = observer(function AIProfilesPanel() { const translate = useTranslate(); const notificationService = useService(NotificationService); - const profilesLoader = useResource(AIProfilesPanel, UserAIProfileResource, CachedMapAllKey); + const profilesLoader = useResource(AIProfilesPanel, AIProfilesResource, CachedMapAllKey); const enginesLoader = useResource(AIProfilesPanel, AiEnginesResource, undefined); const profiles = profilesLoader.data.filter((profile): profile is IAIProfile => profile !== undefined); diff --git a/webapp/packages/plugin-ai-user-profile/src/components/AIProfilesTable.tsx b/webapp/packages/plugin-ai-user-profile/src/components/AIProfilesTable.tsx index 6563cc2945b..64df165586b 100644 --- a/webapp/packages/plugin-ai-user-profile/src/components/AIProfilesTable.tsx +++ b/webapp/packages/plugin-ai-user-profile/src/components/AIProfilesTable.tsx @@ -20,7 +20,8 @@ import { } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; -import { AIProfileCredentialsDialogService, type EngineInfo } from '@cloudbeaver/plugin-ai'; +import type { EngineInfo } from '@cloudbeaver/plugin-ai'; +import { AIProfileCredentialsService } from '@cloudbeaver/plugin-ai-profiles'; export interface IAIProfile { id: string; @@ -37,12 +38,12 @@ interface Props { export const AIProfilesTable = observer(function AIProfilesTable({ profiles, engines }) { const translate = useTranslate(); - const credentialsDialogService = useService(AIProfileCredentialsDialogService); + const credentialsService = useService(AIProfileCredentialsService); const notificationService = useService(NotificationService); async function editCredentials(profileId: string): Promise { try { - await credentialsDialogService.open(profileId); + await credentialsService.open(profileId); } catch (exception: any) { notificationService.logException(exception, 'plugin_ai_user_profile_credentials_edit_failed'); } diff --git a/webapp/packages/plugin-ai-user-profile/tsconfig.json b/webapp/packages/plugin-ai-user-profile/tsconfig.json index 038a9d1f009..3cfc2d0a8e8 100644 --- a/webapp/packages/plugin-ai-user-profile/tsconfig.json +++ b/webapp/packages/plugin-ai-user-profile/tsconfig.json @@ -34,6 +34,9 @@ { "path": "../plugin-ai" }, + { + "path": "../plugin-ai-profiles" + }, { "path": "../plugin-user-profile" } diff --git a/webapp/packages/plugin-ai/package.json b/webapp/packages/plugin-ai/package.json index 2a176451db0..843a0286eb0 100644 --- a/webapp/packages/plugin-ai/package.json +++ b/webapp/packages/plugin-ai/package.json @@ -21,25 +21,15 @@ "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { - "@cloudbeaver/core-authentication": "workspace:*", - "@cloudbeaver/core-blocks": "workspace:*", "@cloudbeaver/core-di": "workspace:*", - "@cloudbeaver/core-dialogs": "workspace:*", - "@cloudbeaver/core-events": "workspace:*", - "@cloudbeaver/core-localization": "workspace:*", "@cloudbeaver/core-resource": "workspace:*", "@cloudbeaver/core-root": "workspace:*", "@cloudbeaver/core-sdk": "workspace:*", - "mobx": "^6", - "mobx-react-lite": "^4", - "react": "^19", - "react-dom": "^19", "tslib": "^2" }, "devDependencies": { "@cloudbeaver/core-cli": "workspace:*", "@cloudbeaver/tsconfig": "workspace:*", - "@types/react": "^19", "rimraf": "^6", "typescript": "^5" } diff --git a/webapp/packages/plugin-ai/src/AISettingsResource.ts b/webapp/packages/plugin-ai/src/AISettingsResource.ts index f177d16c8e5..f4cb5926c0b 100644 --- a/webapp/packages/plugin-ai/src/AISettingsResource.ts +++ b/webapp/packages/plugin-ai/src/AISettingsResource.ts @@ -9,7 +9,7 @@ import { injectable } from '@cloudbeaver/core-di'; import { CachedDataResource } from '@cloudbeaver/core-resource'; import { ServerConfigResource, ServerEventId, WorkspaceConfigEventHandler } from '@cloudbeaver/core-root'; -import { type AiSettingsInfo, GraphQLService } from '@cloudbeaver/core-sdk'; +import { type AiSettingsConfig, type AiSettingsInfo, GraphQLService } from '@cloudbeaver/core-sdk'; export type AISettings = AiSettingsInfo; @@ -26,6 +26,13 @@ export class AISettingsResource extends CachedDataResource { workspaceConfigEventHandler.onEvent(ServerEventId.CbWorkspaceConfigChanged, () => this.markOutdated(), undefined, this); } + async saveSettings(settings: AiSettingsConfig): Promise { + await this.performUpdate(undefined, undefined, async () => { + const { result } = await this.graphQLService.sdk.saveAiSettings({ settings }); + this.setData(result); + }); + } + protected async loader(): Promise { const { settings } = await this.graphQLService.sdk.getAiSettings(); return settings; diff --git a/webapp/packages/plugin-ai/src/index.ts b/webapp/packages/plugin-ai/src/index.ts index dc5c5d438ab..97743bdd889 100644 --- a/webapp/packages/plugin-ai/src/index.ts +++ b/webapp/packages/plugin-ai/src/index.ts @@ -9,9 +9,4 @@ import './module.js'; export * from './AiEnginesResource.js'; -export * from './AIProfileCredentialsDialogLazy.js'; -export * from './AIProfileCredentialsService.js'; -export * from './AIProfileCredentialsUtils.js'; -export * from './UserAIProfileResource.js'; export * from './AISettingsResource.js'; -export * from './IAIProfileCredentialsDialogPayload.js'; diff --git a/webapp/packages/plugin-ai/src/module.ts b/webapp/packages/plugin-ai/src/module.ts index 54948fb2ff0..13da690e905 100644 --- a/webapp/packages/plugin-ai/src/module.ts +++ b/webapp/packages/plugin-ai/src/module.ts @@ -6,26 +6,19 @@ * you may not use this file except in compliance with the License. */ -import { Bootstrap, Dependency, ModuleRegistry, proxy } from '@cloudbeaver/core-di'; +import { Dependency, ModuleRegistry, proxy } from '@cloudbeaver/core-di'; import { AiEnginesResource } from './AiEnginesResource.js'; -import { AIProfileCredentialsService } from './AIProfileCredentialsService.js'; -import { UserAIProfileResource } from './UserAIProfileResource.js'; import { AISettingsResource } from './AISettingsResource.js'; -import { LocaleService } from './LocaleService.js'; export default ModuleRegistry.add({ name: '@cloudbeaver/plugin-ai', configure: serviceCollection => { serviceCollection - .addSingleton(Bootstrap, LocaleService) .addSingleton(Dependency, proxy(AiEnginesResource)) - .addSingleton(Dependency, proxy(UserAIProfileResource)) .addSingleton(Dependency, proxy(AISettingsResource)) .addSingleton(AiEnginesResource) - .addSingleton(UserAIProfileResource) - .addSingleton(AISettingsResource) - .addSingleton(AIProfileCredentialsService); + .addSingleton(AISettingsResource); }, }); diff --git a/webapp/packages/plugin-ai/tsconfig.json b/webapp/packages/plugin-ai/tsconfig.json index f41ca33ed46..a68c576f611 100644 --- a/webapp/packages/plugin-ai/tsconfig.json +++ b/webapp/packages/plugin-ai/tsconfig.json @@ -11,27 +11,12 @@ "**/node_modules" ], "references": [ - { - "path": "../core-authentication" - }, - { - "path": "../core-blocks" - }, { "path": "../core-cli" }, { "path": "../core-di" }, - { - "path": "../core-dialogs" - }, - { - "path": "../core-events" - }, - { - "path": "../core-localization" - }, { "path": "../core-resource" }, diff --git a/webapp/packages/plugin-set-common/package.json b/webapp/packages/plugin-set-common/package.json index d0e739007ee..5e480c89f5d 100644 --- a/webapp/packages/plugin-set-common/package.json +++ b/webapp/packages/plugin-set-common/package.json @@ -59,6 +59,8 @@ "@cloudbeaver/plugin-ai": "workspace:*", "@cloudbeaver/plugin-ai-administration": "workspace:*", "@cloudbeaver/plugin-ai-chat": "workspace:*", + "@cloudbeaver/plugin-ai-profiles": "workspace:*", + "@cloudbeaver/plugin-ai-profiles-administration": "workspace:*", "@cloudbeaver/plugin-ai-user-profile": "workspace:*", "@cloudbeaver/plugin-app-logo": "workspace:*", "@cloudbeaver/plugin-app-logo-administration": "workspace:*", diff --git a/webapp/packages/plugin-set-common/src/index.ts b/webapp/packages/plugin-set-common/src/index.ts index 3c6a938fef0..e8df6cc876b 100644 --- a/webapp/packages/plugin-set-common/src/index.ts +++ b/webapp/packages/plugin-set-common/src/index.ts @@ -118,9 +118,11 @@ import pluginConnectionPreferences from '@cloudbeaver/plugin-connection-preferen import pluginScriptExport from '@cloudbeaver/plugin-script-export/module'; import pluginProjectInfo from '@cloudbeaver/plugin-project-info/module'; import pluginDataViewerReferences from '@cloudbeaver/plugin-data-viewer-references/module'; -import pluginAiChat from '@cloudbeaver/plugin-ai-chat/module'; import pluginAi from '@cloudbeaver/plugin-ai/module'; +import pluginAiProfiles from '@cloudbeaver/plugin-ai-profiles/module'; +import pluginAiChat from '@cloudbeaver/plugin-ai-chat/module'; import pluginAiAdministration from '@cloudbeaver/plugin-ai-administration/module'; +import pluginAiProfilesAdministration from '@cloudbeaver/plugin-ai-profiles-administration/module'; import pluginAiUserProfile from '@cloudbeaver/plugin-ai-user-profile/module'; import pluginConnectionFormAi from '@cloudbeaver/plugin-connection-form-ai/module'; @@ -240,9 +242,11 @@ export const commonSet = [ pluginNetworkHandlers, pluginConnectionNetworkHandlers, pluginDataViewerReferences, - pluginAiChat, pluginAi, + pluginAiProfiles, + pluginAiChat, pluginAiAdministration, + pluginAiProfilesAdministration, pluginAiUserProfile, pluginConnectionFormAi, ]; diff --git a/webapp/packages/plugin-set-common/tsconfig.json b/webapp/packages/plugin-set-common/tsconfig.json index cd5e8fbf4f3..6b979c90414 100644 --- a/webapp/packages/plugin-set-common/tsconfig.json +++ b/webapp/packages/plugin-set-common/tsconfig.json @@ -124,6 +124,12 @@ { "path": "../plugin-ai-chat" }, + { + "path": "../plugin-ai-profiles" + }, + { + "path": "../plugin-ai-profiles-administration" + }, { "path": "../plugin-ai-user-profile" }, diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 2d6b25783dd..822de1f0b58 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -2477,14 +2477,12 @@ __metadata: "@cloudbeaver/core-localization": "workspace:*" "@cloudbeaver/core-resource": "workspace:*" "@cloudbeaver/core-root": "workspace:*" - "@cloudbeaver/core-sdk": "workspace:*" "@cloudbeaver/core-ui": "workspace:*" "@cloudbeaver/core-utils": "workspace:*" "@cloudbeaver/plugin-ai": "workspace:*" - "@cloudbeaver/plugin-data-grid": "workspace:*" + "@cloudbeaver/plugin-ai-profiles": "workspace:*" "@cloudbeaver/tsconfig": "workspace:*" "@dbeaver/js-helpers": "workspace:*" - "@dbeaver/ui-kit": "workspace:*" "@types/react": "npm:^19" mobx: "npm:^6" mobx-react-lite: "npm:^4" @@ -2493,6 +2491,7 @@ __metadata: rimraf: "npm:^6" tslib: "npm:^2" typescript: "npm:^5" + typescript-plugin-css-modules: "npm:^5" languageName: unknown linkType: soft @@ -2519,6 +2518,7 @@ __metadata: "@cloudbeaver/core-utils": "workspace:*" "@cloudbeaver/core-view": "workspace:*" "@cloudbeaver/plugin-ai": "workspace:*" + "@cloudbeaver/plugin-ai-profiles": "workspace:*" "@cloudbeaver/plugin-codemirror6": "workspace:*" "@cloudbeaver/plugin-datasource-context-switch": "workspace:*" "@cloudbeaver/plugin-navigation-tabs": "workspace:*" @@ -2546,21 +2546,31 @@ __metadata: languageName: unknown linkType: soft -"@cloudbeaver/plugin-ai-user-profile@workspace:*, @cloudbeaver/plugin-ai-user-profile@workspace:packages/plugin-ai-user-profile": +"@cloudbeaver/plugin-ai-profiles-administration@workspace:*, @cloudbeaver/plugin-ai-profiles-administration@workspace:packages/plugin-ai-profiles-administration": version: 0.0.0-use.local - resolution: "@cloudbeaver/plugin-ai-user-profile@workspace:packages/plugin-ai-user-profile" + resolution: "@cloudbeaver/plugin-ai-profiles-administration@workspace:packages/plugin-ai-profiles-administration" dependencies: - "@cloudbeaver/core-authentication": "workspace:*" + "@cloudbeaver/core-administration": "workspace:*" "@cloudbeaver/core-blocks": "workspace:*" "@cloudbeaver/core-cli": "workspace:*" + "@cloudbeaver/core-data-context": "workspace:*" "@cloudbeaver/core-di": "workspace:*" + "@cloudbeaver/core-dialogs": "workspace:*" "@cloudbeaver/core-events": "workspace:*" + "@cloudbeaver/core-executor": "workspace:*" "@cloudbeaver/core-localization": "workspace:*" "@cloudbeaver/core-resource": "workspace:*" "@cloudbeaver/core-root": "workspace:*" + "@cloudbeaver/core-sdk": "workspace:*" + "@cloudbeaver/core-ui": "workspace:*" + "@cloudbeaver/core-utils": "workspace:*" "@cloudbeaver/plugin-ai": "workspace:*" - "@cloudbeaver/plugin-user-profile": "workspace:*" + "@cloudbeaver/plugin-ai-administration": "workspace:*" + "@cloudbeaver/plugin-ai-profiles": "workspace:*" + "@cloudbeaver/plugin-data-grid": "workspace:*" "@cloudbeaver/tsconfig": "workspace:*" + "@dbeaver/js-helpers": "workspace:*" + "@dbeaver/ui-kit": "workspace:*" "@types/react": "npm:^19" mobx: "npm:^6" mobx-react-lite: "npm:^4" @@ -2569,12 +2579,13 @@ __metadata: rimraf: "npm:^6" tslib: "npm:^2" typescript: "npm:^5" + typescript-plugin-css-modules: "npm:^5" languageName: unknown linkType: soft -"@cloudbeaver/plugin-ai@workspace:*, @cloudbeaver/plugin-ai@workspace:packages/plugin-ai": +"@cloudbeaver/plugin-ai-profiles@workspace:*, @cloudbeaver/plugin-ai-profiles@workspace:packages/plugin-ai-profiles": version: 0.0.0-use.local - resolution: "@cloudbeaver/plugin-ai@workspace:packages/plugin-ai" + resolution: "@cloudbeaver/plugin-ai-profiles@workspace:packages/plugin-ai-profiles" dependencies: "@cloudbeaver/core-authentication": "workspace:*" "@cloudbeaver/core-blocks": "workspace:*" @@ -2586,6 +2597,7 @@ __metadata: "@cloudbeaver/core-resource": "workspace:*" "@cloudbeaver/core-root": "workspace:*" "@cloudbeaver/core-sdk": "workspace:*" + "@cloudbeaver/plugin-ai": "workspace:*" "@cloudbeaver/tsconfig": "workspace:*" "@types/react": "npm:^19" mobx: "npm:^6" @@ -2598,6 +2610,49 @@ __metadata: languageName: unknown linkType: soft +"@cloudbeaver/plugin-ai-user-profile@workspace:*, @cloudbeaver/plugin-ai-user-profile@workspace:packages/plugin-ai-user-profile": + version: 0.0.0-use.local + resolution: "@cloudbeaver/plugin-ai-user-profile@workspace:packages/plugin-ai-user-profile" + dependencies: + "@cloudbeaver/core-authentication": "workspace:*" + "@cloudbeaver/core-blocks": "workspace:*" + "@cloudbeaver/core-cli": "workspace:*" + "@cloudbeaver/core-di": "workspace:*" + "@cloudbeaver/core-events": "workspace:*" + "@cloudbeaver/core-localization": "workspace:*" + "@cloudbeaver/core-resource": "workspace:*" + "@cloudbeaver/core-root": "workspace:*" + "@cloudbeaver/plugin-ai": "workspace:*" + "@cloudbeaver/plugin-ai-profiles": "workspace:*" + "@cloudbeaver/plugin-user-profile": "workspace:*" + "@cloudbeaver/tsconfig": "workspace:*" + "@types/react": "npm:^19" + mobx: "npm:^6" + mobx-react-lite: "npm:^4" + react: "npm:^19" + react-dom: "npm:^19" + rimraf: "npm:^6" + tslib: "npm:^2" + typescript: "npm:^5" + languageName: unknown + linkType: soft + +"@cloudbeaver/plugin-ai@workspace:*, @cloudbeaver/plugin-ai@workspace:packages/plugin-ai": + version: 0.0.0-use.local + resolution: "@cloudbeaver/plugin-ai@workspace:packages/plugin-ai" + dependencies: + "@cloudbeaver/core-cli": "workspace:*" + "@cloudbeaver/core-di": "workspace:*" + "@cloudbeaver/core-resource": "workspace:*" + "@cloudbeaver/core-root": "workspace:*" + "@cloudbeaver/core-sdk": "workspace:*" + "@cloudbeaver/tsconfig": "workspace:*" + rimraf: "npm:^6" + tslib: "npm:^2" + typescript: "npm:^5" + languageName: unknown + linkType: soft + "@cloudbeaver/plugin-app-logo-administration@workspace:*, @cloudbeaver/plugin-app-logo-administration@workspace:packages/plugin-app-logo-administration": version: 0.0.0-use.local resolution: "@cloudbeaver/plugin-app-logo-administration@workspace:packages/plugin-app-logo-administration" @@ -4311,6 +4366,8 @@ __metadata: "@cloudbeaver/plugin-ai": "workspace:*" "@cloudbeaver/plugin-ai-administration": "workspace:*" "@cloudbeaver/plugin-ai-chat": "workspace:*" + "@cloudbeaver/plugin-ai-profiles": "workspace:*" + "@cloudbeaver/plugin-ai-profiles-administration": "workspace:*" "@cloudbeaver/plugin-ai-user-profile": "workspace:*" "@cloudbeaver/plugin-app-logo": "workspace:*" "@cloudbeaver/plugin-app-logo-administration": "workspace:*" From 714fe2d1b00bade29cf709fb810aa55cc49005ba Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Wed, 2 Sep 2026 21:55:52 +0200 Subject: [PATCH 04/31] dbeaver/pro#9532 adds dialog ux improvements --- .../src/AIProfileCredentialsDialog.tsx | 98 +++++++++++-------- 1 file changed, 55 insertions(+), 43 deletions(-) diff --git a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx index c8dd2ea599d..2df0f0f250b 100644 --- a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx +++ b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx @@ -18,8 +18,11 @@ import { ConfirmationDialog, Container, Fill, + Form, InputField, SAVED_VALUE_INDICATOR, + useFocus, + useForm, useObservableRef, useTranslate, } from '@cloudbeaver/core-blocks'; @@ -45,13 +48,19 @@ export const AIProfileCredentialsDialog: DialogComponent({ autofocus: true }); const state = useObservableRef( () => ({ token: '', processing: false, credentialsSaved: payload.credentialsSaved }), { token: observable.ref, processing: observable.ref, credentialsSaved: observable.ref }, false, ); + const form = useForm({ onSubmit: save }); async function save(): Promise { + if (state.processing || !state.token) { + return; + } + try { state.processing = true; if (state.token) { @@ -97,49 +106,52 @@ export const AIProfileCredentialsDialog: DialogComponent - - - - - {translate('plugin_ai_credentials_profile')} - - - {translate('plugin_ai_credentials_engine')} - - - {translate('plugin_ai_credentials_token')} - - - - - {state.credentialsSaved && ( - + )} + + + - )} - - - - - + + + ); }); From 2bd0b432e1fc970ccf9e6b22dd6b2ede6bc1a954 Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Wed, 2 Sep 2026 21:57:10 +0200 Subject: [PATCH 05/31] dbeaver/pro#9532 cleanup --- .../plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx index 2df0f0f250b..962ebfb5b88 100644 --- a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx +++ b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx @@ -147,7 +147,7 @@ export const AIProfileCredentialsDialog: DialogComponent rejectDialog()}> {translate('ui_processing_cancel')} - From 3abec21a3962e49b8e62f5fdbf98100cc1aaf82d Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Thu, 3 Sep 2026 14:33:46 +0200 Subject: [PATCH 06/31] dbeaver/pro#9532 build fix --- .../src/AIProfiles/AIProfileForm/Options/AIProfileFormPart.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfileFormPart.ts b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfileFormPart.ts index 0f3886f7391..a25628723c7 100644 --- a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfileFormPart.ts +++ b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfileFormPart.ts @@ -18,6 +18,7 @@ import { getObjectPropertiesValues } from '../../utils/getObjectPropertiesValues import { prepareProperties } from '../../utils/prepareProperties.js'; import type { IAIProfileFormState } from '../IAIProfileFormState.js'; import type { IAIProfileOptionsState } from './AIProfileSchema.js'; +import type { AIProfileCredentialsService, AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; const GLOBAL_PROPERTY_ID = 'global'; From b94172c3e48aa367ad036d8712b25399bd6011b3 Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Thu, 3 Sep 2026 15:08:30 +0200 Subject: [PATCH 07/31] dbeaver/pro#9532 adds shared table for profiles (user info & ai settings) --- .../package.json | 2 - .../AIProfilesAdministrationTable.tsx | 50 +++++ .../src/AIProfiles/AIProfilesPanel.tsx | 4 +- .../src/AIProfiles/AIProfilesTable.tsx | 167 --------------- .../tsconfig.json | 6 - .../packages/plugin-ai-profiles/package.json | 5 +- .../src/AIProfilesTable.module.css | 23 ++ .../src/AIProfilesTable.tsx | 200 ++++++++++++++++++ .../src/AI_PROFILES_TABLE_ROW_HEIGHT.ts | 9 + .../packages/plugin-ai-profiles/src/index.ts | 1 + .../packages/plugin-ai-profiles/tsconfig.json | 6 + .../src/components/AIProfilesPanel.tsx | 8 +- .../src/components/AIProfilesTable.tsx | 110 ---------- .../src/components/AIUserProfilesTable.tsx | 70 ++++++ webapp/yarn.lock | 5 +- 15 files changed, 372 insertions(+), 294 deletions(-) create mode 100644 webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesAdministrationTable.tsx delete mode 100644 webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesTable.tsx create mode 100644 webapp/packages/plugin-ai-profiles/src/AIProfilesTable.module.css create mode 100644 webapp/packages/plugin-ai-profiles/src/AIProfilesTable.tsx create mode 100644 webapp/packages/plugin-ai-profiles/src/AI_PROFILES_TABLE_ROW_HEIGHT.ts delete mode 100644 webapp/packages/plugin-ai-user-profile/src/components/AIProfilesTable.tsx create mode 100644 webapp/packages/plugin-ai-user-profile/src/components/AIUserProfilesTable.tsx diff --git a/webapp/packages/plugin-ai-profiles-administration/package.json b/webapp/packages/plugin-ai-profiles-administration/package.json index f961d0ee61d..52f5d298368 100644 --- a/webapp/packages/plugin-ai-profiles-administration/package.json +++ b/webapp/packages/plugin-ai-profiles-administration/package.json @@ -21,7 +21,6 @@ "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { - "@cloudbeaver/core-administration": "workspace:*", "@cloudbeaver/core-blocks": "workspace:*", "@cloudbeaver/core-data-context": "workspace:*", "@cloudbeaver/core-di": "workspace:*", @@ -39,7 +38,6 @@ "@cloudbeaver/plugin-ai-profiles": "workspace:*", "@cloudbeaver/plugin-data-grid": "workspace:*", "@dbeaver/js-helpers": "workspace:*", - "@dbeaver/ui-kit": "workspace:*", "mobx": "^6", "mobx-react-lite": "^4", "react": "^19", diff --git a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesAdministrationTable.tsx b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesAdministrationTable.tsx new file mode 100644 index 00000000000..3ee7a1566c1 --- /dev/null +++ b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesAdministrationTable.tsx @@ -0,0 +1,50 @@ +/* + * 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 { useService } from '@cloudbeaver/core-di'; +import { useTranslate } from '@cloudbeaver/core-blocks'; +import { AIProfilesTable, type AIProfile } from '@cloudbeaver/plugin-ai-profiles'; + +import { AIProfileFormService } from './AIProfileForm/AIProfileFormService.js'; + +interface Props { + profiles: AIProfile[]; + deletionDisabled: boolean; + isDefaultProfile: (profileId: string) => boolean; +} + +export const AIProfilesAdministrationTable = observer(function AIProfilesAdministrationTable({ + profiles, + deletionDisabled, + isDefaultProfile, +}) { + const translate = useTranslate(); + const aiProfileFormService = useService(AIProfileFormService); + + return ( + !isDefaultProfile(profile.id)} + getSelectionTitle={profile => + isDefaultProfile(profile.id) ? translate('plugin_ai_administration_profile_default_delete_info') : undefined + } + getProfileBadge={profile => + isDefaultProfile(profile.id) ? ( + {translate('plugin_ai_administration_profile_default_badge')} + ) : null + } + onProfileClick={profile => aiProfileFormService.open(profile.id, profile.name)} + /> + ); +}); diff --git a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesPanel.tsx b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesPanel.tsx index 6871dd4c78e..301da517bfb 100644 --- a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesPanel.tsx +++ b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesPanel.tsx @@ -31,7 +31,7 @@ import { isDefined } from '@dbeaver/js-helpers'; import { AIProfileFormService } from './AIProfileForm/AIProfileFormService.js'; import AIProfilesToolsPanelStyles from './AIProfilesToolsPanel.module.css'; -import { AIProfilesTable } from './AIProfilesTable.js'; +import { AIProfilesAdministrationTable } from './AIProfilesAdministrationTable.js'; import { useAIProfilesTable } from './useAIProfilesTable.js'; const toolsPanelRegistry: StyleRegistry = [ @@ -92,7 +92,7 @@ export const AIProfilesPanel = observer(function AIProfilesPanel() { - aiSettingsService.isEffectiveDefaultProfile(profileId)} diff --git a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesTable.tsx b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesTable.tsx deleted file mode 100644 index c58e9c70052..00000000000 --- a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesTable.tsx +++ /dev/null @@ -1,167 +0,0 @@ -/* - * 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 { reaction } from 'mobx'; -import { observer } from 'mobx-react-lite'; - -import { IconOrImage, Link, s, TextPlaceholder, useResource, useS, useTranslate } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; -import { ADMINISTRATION_TABLE_DEFAULT_ROW_HEIGHT, AdministrationTableStyles } from '@cloudbeaver/core-administration'; -import { DataGrid, TableRowSelect, useCreateGridReactiveValue } from '@cloudbeaver/plugin-data-grid'; -import { AiEnginesResource } from '@cloudbeaver/plugin-ai'; -import type { AIProfile } from '@cloudbeaver/plugin-ai-profiles'; -import { Command } from '@dbeaver/ui-kit'; - -import { AIProfileFormService } from './AIProfileForm/AIProfileFormService.js'; -interface Props { - profiles: AIProfile[]; - deletionDisabled: boolean; - isDefaultProfile: (profileId: string) => boolean; -} - -const ENGINE_COLUMN_WIDTH = 160; - -const SELECT_COLUMN = { key: 'select', label: '' }; -const NAME_COLUMN = { key: 'name', label: 'plugin_ai_administration_profile_column_name' }; -const ENGINE_COLUMN = { key: 'engine', label: 'plugin_ai_administration_profile_column_engine' }; - -const COLUMNS = [SELECT_COLUMN, NAME_COLUMN, ENGINE_COLUMN]; - -export const AIProfilesTable = observer(function AIProfilesTable({ profiles, deletionDisabled, isDefaultProfile }) { - const translate = useTranslate(); - const styles = useS(AdministrationTableStyles); - const aiProfileFormService = useService(AIProfileFormService); - const enginesLoader = useResource(AIProfilesTable, AiEnginesResource, undefined); - - const columnsCount = useCreateGridReactiveValue(() => COLUMNS.length, null, [COLUMNS]); - const rowsCount = useCreateGridReactiveValue( - () => profiles.length, - onValueChange => reaction(() => profiles.length, onValueChange), - [profiles], - ); - - function getCell(rowIdx: number, colIdx: number) { - const profile = profiles[rowIdx]; - const column = COLUMNS[colIdx]; - - if (!profile || !column) { - return null; - } - - const isDefault = isDefaultProfile(profile.id); - - if (column.key === SELECT_COLUMN.key) { - return ( - - ); - } - - if (column.key === NAME_COLUMN.key) { - return ( - } - tabIndex={0} - title={profile.name} - className="tw:flex tw:cursor-pointer tw:items-center tw:gap-2 tw:outline-none" - onClick={() => aiProfileFormService.open(profile.id, profile.name)} - > - {profile.name} - {isDefault && ( - {translate('plugin_ai_administration_profile_default_badge')} - )} - {profile.global && } - - ); - } - - if (column.key === ENGINE_COLUMN.key) { - const engine = enginesLoader.data.find(engine => engine.id === profile.engineId); - const title = engine?.name ?? profile.engineId; - - if (engine?.icon) { - return ( -
- {title} - -
- ); - } - - return {title}; - } - - return null; - } - - const cell = useCreateGridReactiveValue(getCell, (onValueChange, rowIdx, colIdx) => reaction(() => getCell(rowIdx, colIdx), onValueChange), [ - COLUMNS, - profiles, - deletionDisabled, - isDefaultProfile, - aiProfileFormService, - enginesLoader.data, - ]); - - function getHeaderText(colIdx: number) { - return translate(COLUMNS[colIdx]?.label) ?? ''; - } - - function getHeaderElement(colIdx: number) { - if (colIdx === 0) { - return ; - } - - return getHeaderText(colIdx); - } - - const headerElement = useCreateGridReactiveValue( - getHeaderElement, - (onValueChange, colIdx) => reaction(() => getHeaderElement(colIdx), onValueChange), - [COLUMNS, translate], - ); - - const headerText = useCreateGridReactiveValue(getHeaderText, (onValueChange, colIdx) => reaction(() => getHeaderText(colIdx), onValueChange), [ - COLUMNS, - translate, - ]); - - if (!profiles.length) { - return {translate('plugin_ai_administration_profiles_table_empty_placeholder')}; - } - - function getHeaderWidth(idx: number) { - const column = COLUMNS[idx]; - - if (column?.key === ENGINE_COLUMN.key) { - return ENGINE_COLUMN_WIDTH; - } - - return null; - } - - return ( -
- colIdx > 0} - getRowHeight={() => ADMINISTRATION_TABLE_DEFAULT_ROW_HEIGHT} - getHeaderPinned={colIdx => colIdx <= 0} - headerText={headerText} - headerElement={headerElement} - getHeaderWidth={getHeaderWidth} - cell={cell} - className={s(styles, { table: true })} - /> -
- ); -}); diff --git a/webapp/packages/plugin-ai-profiles-administration/tsconfig.json b/webapp/packages/plugin-ai-profiles-administration/tsconfig.json index f73f5ae9f62..bcc17bea522 100644 --- a/webapp/packages/plugin-ai-profiles-administration/tsconfig.json +++ b/webapp/packages/plugin-ai-profiles-administration/tsconfig.json @@ -7,15 +7,9 @@ "composite": true }, "references": [ - { - "path": "../../common-react/@dbeaver/ui-kit" - }, { "path": "../../common-typescript/@dbeaver/js-helpers" }, - { - "path": "../core-administration" - }, { "path": "../core-blocks" }, diff --git a/webapp/packages/plugin-ai-profiles/package.json b/webapp/packages/plugin-ai-profiles/package.json index 2bf193f91d7..5f49201fc5d 100644 --- a/webapp/packages/plugin-ai-profiles/package.json +++ b/webapp/packages/plugin-ai-profiles/package.json @@ -31,6 +31,8 @@ "@cloudbeaver/core-root": "workspace:*", "@cloudbeaver/core-sdk": "workspace:*", "@cloudbeaver/plugin-ai": "workspace:*", + "@cloudbeaver/plugin-data-grid": "workspace:*", + "@dbeaver/ui-kit": "workspace:*", "mobx": "^6", "mobx-react-lite": "^4", "react": "^19", @@ -42,6 +44,7 @@ "@cloudbeaver/tsconfig": "workspace:*", "@types/react": "^19", "rimraf": "^6", - "typescript": "^5" + "typescript": "^5", + "typescript-plugin-css-modules": "^5" } } diff --git a/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.module.css b/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.module.css new file mode 100644 index 00000000000..5f33db4ab0d --- /dev/null +++ b/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.module.css @@ -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. + */ + +.table { + font-size: 14px; + + & [role='columnheader'] { + text-transform: uppercase; + } + + :global(.rdg-cell) { + padding-inline: 12px; + } + + :global(.rdg-cell):not([role='columnheader']):not(:last-child) { + border-inline-end: 1px solid transparent; + } +} diff --git a/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.tsx b/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.tsx new file mode 100644 index 00000000000..c100bc5d400 --- /dev/null +++ b/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.tsx @@ -0,0 +1,200 @@ +/* + * 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 { reaction } from 'mobx'; +import { observer } from 'mobx-react-lite'; +import type { ReactNode } from 'react'; + +import { IconOrImage, Link, s, TextPlaceholder, useResource, useS, useTranslate } from '@cloudbeaver/core-blocks'; +import { AiEnginesResource } from '@cloudbeaver/plugin-ai'; +import { DataGrid, TableRowSelect, useCreateGridReactiveValue } from '@cloudbeaver/plugin-data-grid'; +import { Command } from '@dbeaver/ui-kit'; + +import { AI_PROFILES_TABLE_ROW_HEIGHT } from './AI_PROFILES_TABLE_ROW_HEIGHT.js'; +import type { AIProfile } from './AIProfilesResource.js'; +import AIProfilesTableStyles from './AIProfilesTable.module.css'; + +export interface IAIProfilesTableColumn { + key: string; + label: string; + width?: number; + render: (profile: AIProfile) => ReactNode; +} + +type TableColumn = Omit & { render?: IAIProfilesTableColumn['render'] }; + +interface Props { + profiles: AIProfile[]; + nameLabel: string; + engineLabel: string; + emptyPlaceholder: string; + additionalColumns?: IAIProfilesTableColumn[]; + selectionDisabled?: boolean; + isProfileSelectable?: (profile: AIProfile) => boolean; + getSelectionTitle?: (profile: AIProfile) => string | undefined; + isProfileClickable?: (profile: AIProfile) => boolean; + getProfileBadge?: (profile: AIProfile) => ReactNode; + onProfileClick?: (profile: AIProfile) => void; +} + +const SELECT_COLUMN = { key: 'select', label: '' }; +const NAME_COLUMN = { key: 'name' }; +const ENGINE_COLUMN = { key: 'engine', width: 160 }; + +export const AIProfilesTable = observer(function AIProfilesTable({ + profiles, + nameLabel, + engineLabel, + emptyPlaceholder, + additionalColumns = [], + selectionDisabled, + isProfileSelectable, + getSelectionTitle, + isProfileClickable, + getProfileBadge, + onProfileClick, +}) { + const translate = useTranslate(); + const styles = useS(AIProfilesTableStyles); + const enginesLoader = useResource(AIProfilesTable, AiEnginesResource, undefined); + const selectable = !!isProfileSelectable; + const columns: TableColumn[] = [ + ...(selectable ? [SELECT_COLUMN] : []), + { ...NAME_COLUMN, label: nameLabel }, + { ...ENGINE_COLUMN, label: engineLabel }, + ...additionalColumns, + ]; + + const columnCount = useCreateGridReactiveValue(() => columns.length, null, [columns]); + const rowCount = useCreateGridReactiveValue( + () => profiles.length, + onValueChange => reaction(() => profiles.length, onValueChange), + [profiles], + ); + + function getCell(rowIdx: number, colIdx: number) { + const profile = profiles[rowIdx]; + const column = columns[colIdx]; + if (!profile || !column) { + return null; + } + + if (column.key === SELECT_COLUMN.key) { + return ( + + ); + } + + if (column.key === NAME_COLUMN.key) { + const clickable = !!onProfileClick && (isProfileClickable?.(profile) ?? true); + const content = ( + <> + {clickable ? {profile.name} : {profile.name}} + {getProfileBadge?.(profile)} + {profile.global && } + + ); + + if (clickable) { + return ( + } + tabIndex={0} + title={profile.name} + className="tw:flex tw:cursor-pointer tw:items-center tw:gap-2 tw:outline-none" + onClick={() => onProfileClick(profile)} + > + {content} + + ); + } + + return ( +
+ {content} +
+ ); + } + + if (column.key === ENGINE_COLUMN.key) { + const engine = enginesLoader.data.find(engine => engine.id === profile.engineId); + const title = engine?.name ?? profile.engineId; + return ( +
+ {title} + {engine?.icon && } +
+ ); + } + + return column.render?.(profile) ?? null; + } + + const cell = useCreateGridReactiveValue(getCell, (onValueChange, rowIdx, colIdx) => reaction(() => getCell(rowIdx, colIdx), onValueChange), [ + profiles, + columns, + enginesLoader.data, + additionalColumns, + selectionDisabled, + isProfileSelectable, + getSelectionTitle, + isProfileClickable, + getProfileBadge, + onProfileClick, + ]); + + function getHeaderText(colIdx: number) { + return translate(columns[colIdx]?.label) ?? ''; + } + + function getHeaderElement(colIdx: number) { + if (columns[colIdx]?.key === SELECT_COLUMN.key) { + return ; + } + return getHeaderText(colIdx); + } + + const headerElement = useCreateGridReactiveValue( + getHeaderElement, + (onValueChange, colIdx) => reaction(() => getHeaderElement(colIdx), onValueChange), + [columns, translate], + ); + const headerText = useCreateGridReactiveValue(getHeaderText, (onValueChange, colIdx) => reaction(() => getHeaderText(colIdx), onValueChange), [ + columns, + translate, + ]); + + if (!profiles.length) { + return {translate(emptyPlaceholder)}; + } + + function getHeaderWidth(colIdx: number) { + return columns[colIdx]?.width ?? null; + } + + return ( +
+ columns[colIdx]?.key !== SELECT_COLUMN.key} + getRowHeight={() => AI_PROFILES_TABLE_ROW_HEIGHT} + getHeaderPinned={colIdx => columns[colIdx]?.key === SELECT_COLUMN.key} + headerText={headerText} + headerElement={headerElement} + getHeaderWidth={getHeaderWidth} + cell={cell} + className={s(styles, { table: true })} + /> +
+ ); +}); diff --git a/webapp/packages/plugin-ai-profiles/src/AI_PROFILES_TABLE_ROW_HEIGHT.ts b/webapp/packages/plugin-ai-profiles/src/AI_PROFILES_TABLE_ROW_HEIGHT.ts new file mode 100644 index 00000000000..4807475346d --- /dev/null +++ b/webapp/packages/plugin-ai-profiles/src/AI_PROFILES_TABLE_ROW_HEIGHT.ts @@ -0,0 +1,9 @@ +/* + * 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 const AI_PROFILES_TABLE_ROW_HEIGHT = 36; diff --git a/webapp/packages/plugin-ai-profiles/src/index.ts b/webapp/packages/plugin-ai-profiles/src/index.ts index b865a36928d..e31d95b0482 100644 --- a/webapp/packages/plugin-ai-profiles/src/index.ts +++ b/webapp/packages/plugin-ai-profiles/src/index.ts @@ -11,4 +11,5 @@ import './module.js'; export * from './AIProfileCredentialsDialogLazy.js'; export * from './AIProfileCredentialsService.js'; export * from './AIProfilesResource.js'; +export * from './AIProfilesTable.js'; export * from './IAIProfileCredentialsDialogPayload.js'; diff --git a/webapp/packages/plugin-ai-profiles/tsconfig.json b/webapp/packages/plugin-ai-profiles/tsconfig.json index e16ea68242d..b39b338b3ef 100644 --- a/webapp/packages/plugin-ai-profiles/tsconfig.json +++ b/webapp/packages/plugin-ai-profiles/tsconfig.json @@ -7,6 +7,9 @@ "composite": true }, "references": [ + { + "path": "../../common-react/@dbeaver/ui-kit" + }, { "path": "../core-authentication" }, @@ -39,6 +42,9 @@ }, { "path": "../plugin-ai" + }, + { + "path": "../plugin-data-grid" } ], "include": [ diff --git a/webapp/packages/plugin-ai-user-profile/src/components/AIProfilesPanel.tsx b/webapp/packages/plugin-ai-user-profile/src/components/AIProfilesPanel.tsx index aa9c0332f11..25d095dac27 100644 --- a/webapp/packages/plugin-ai-user-profile/src/components/AIProfilesPanel.tsx +++ b/webapp/packages/plugin-ai-user-profile/src/components/AIProfilesPanel.tsx @@ -12,16 +12,16 @@ import { CachedMapAllKey } from '@cloudbeaver/core-resource'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { AiEnginesResource } from '@cloudbeaver/plugin-ai'; -import { AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; +import { type AIProfile, AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; -import { AIProfilesTable, type IAIProfile } from './AIProfilesTable.js'; +import { AIUserProfilesTable } from './AIUserProfilesTable.js'; export const AIProfilesPanel = observer(function AIProfilesPanel() { const translate = useTranslate(); const notificationService = useService(NotificationService); const profilesLoader = useResource(AIProfilesPanel, AIProfilesResource, CachedMapAllKey); const enginesLoader = useResource(AIProfilesPanel, AiEnginesResource, undefined); - const profiles = profilesLoader.data.filter((profile): profile is IAIProfile => profile !== undefined); + const profiles = profilesLoader.data.filter((profile): profile is AIProfile => profile !== undefined); async function refresh(): Promise { try { @@ -48,7 +48,7 @@ export const AIProfilesPanel = observer(function AIProfilesPanel() { {profiles.length ? ( - + ) : ( {translate('plugin_ai_user_profile_empty')} )} diff --git a/webapp/packages/plugin-ai-user-profile/src/components/AIProfilesTable.tsx b/webapp/packages/plugin-ai-user-profile/src/components/AIProfilesTable.tsx deleted file mode 100644 index 64df165586b..00000000000 --- a/webapp/packages/plugin-ai-user-profile/src/components/AIProfilesTable.tsx +++ /dev/null @@ -1,110 +0,0 @@ -/* - * 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 { - Button, - IconOrImage, - Table, - TableBody, - TableColumnHeader, - TableColumnValue, - TableHeader, - TableItem, - useTranslate, -} from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; -import { NotificationService } from '@cloudbeaver/core-events'; -import type { EngineInfo } from '@cloudbeaver/plugin-ai'; -import { AIProfileCredentialsService } from '@cloudbeaver/plugin-ai-profiles'; - -export interface IAIProfile { - id: string; - name: string; - engineId: string; - global: boolean; - credentialsSaved: boolean; -} - -interface Props { - profiles: IAIProfile[]; - engines: EngineInfo[]; -} - -export const AIProfilesTable = observer(function AIProfilesTable({ profiles, engines }) { - const translate = useTranslate(); - const credentialsService = useService(AIProfileCredentialsService); - const notificationService = useService(NotificationService); - - async function editCredentials(profileId: string): Promise { - try { - await credentialsService.open(profileId); - } catch (exception: any) { - notificationService.logException(exception, 'plugin_ai_user_profile_credentials_edit_failed'); - } - } - - return ( - profile.id)}> - - {translate('plugin_ai_user_profile_column_profile')} - {translate('plugin_ai_user_profile_column_engine')} - {translate('plugin_ai_user_profile_column_credential_source')} - {translate('plugin_ai_user_profile_column_status')} - - - {profiles.map(profile => { - const engine = engines.find(engine => engine.id === profile.engineId); - const engineName = engine?.name ?? profile.engineId; - - return ( - - -
- {profile.global && } - {profile.name} -
-
- -
- {engine?.icon && } - {engineName} -
-
- - {translate( - profile.global ? 'plugin_ai_user_profile_credential_source_administrator' : 'plugin_ai_user_profile_credential_source_user', - )} - - - {profile.global ? ( - translate('plugin_ai_user_profile_status_managed') - ) : ( -
- - {translate( - profile.credentialsSaved ? 'plugin_ai_user_profile_status_configured' : 'plugin_ai_user_profile_status_not_configured', - )} - - -
- )} -
-
- ); - })} -
-
- ); -}); diff --git a/webapp/packages/plugin-ai-user-profile/src/components/AIUserProfilesTable.tsx b/webapp/packages/plugin-ai-user-profile/src/components/AIUserProfilesTable.tsx new file mode 100644 index 00000000000..5b1da2423ef --- /dev/null +++ b/webapp/packages/plugin-ai-user-profile/src/components/AIUserProfilesTable.tsx @@ -0,0 +1,70 @@ +/* + * 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 { useService } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { useTranslate } from '@cloudbeaver/core-blocks'; +import { + AIProfileCredentialsService, + AIProfilesTable, + type AIProfile, + type IAIProfilesTableColumn, +} from '@cloudbeaver/plugin-ai-profiles'; + +interface Props { + profiles: AIProfile[]; +} + +export const AIUserProfilesTable = observer(function AIUserProfilesTable({ profiles }) { + const translate = useTranslate(); + const credentialsService = useService(AIProfileCredentialsService); + const notificationService = useService(NotificationService); + + async function editCredentials(profileId: string): Promise { + try { + await credentialsService.open(profileId); + } catch (exception: any) { + notificationService.logException(exception, 'plugin_ai_user_profile_credentials_edit_failed'); + } + } + + const columns: IAIProfilesTableColumn[] = [ + { + key: 'credentialSource', + label: 'plugin_ai_user_profile_column_credential_source', + width: 180, + render: profile => + translate(profile.global ? 'plugin_ai_user_profile_credential_source_administrator' : 'plugin_ai_user_profile_credential_source_user'), + }, + { + key: 'status', + label: 'plugin_ai_user_profile_column_status', + width: 280, + render: profile => { + if (profile.global) { + return translate('plugin_ai_user_profile_status_managed'); + } + return translate(profile.credentialsSaved ? 'plugin_ai_user_profile_status_configured' : 'plugin_ai_user_profile_status_not_configured'); + }, + }, + ]; + + return ( + !profile.global} + onProfileClick={profile => editCredentials(profile.id)} + /> + ); +}); diff --git a/webapp/yarn.lock b/webapp/yarn.lock index eb12b9cc766..46f768eeeb7 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -2550,7 +2550,6 @@ __metadata: version: 0.0.0-use.local resolution: "@cloudbeaver/plugin-ai-profiles-administration@workspace:packages/plugin-ai-profiles-administration" dependencies: - "@cloudbeaver/core-administration": "workspace:*" "@cloudbeaver/core-blocks": "workspace:*" "@cloudbeaver/core-cli": "workspace:*" "@cloudbeaver/core-data-context": "workspace:*" @@ -2570,7 +2569,6 @@ __metadata: "@cloudbeaver/plugin-data-grid": "workspace:*" "@cloudbeaver/tsconfig": "workspace:*" "@dbeaver/js-helpers": "workspace:*" - "@dbeaver/ui-kit": "workspace:*" "@types/react": "npm:^19" mobx: "npm:^6" mobx-react-lite: "npm:^4" @@ -2598,7 +2596,9 @@ __metadata: "@cloudbeaver/core-root": "workspace:*" "@cloudbeaver/core-sdk": "workspace:*" "@cloudbeaver/plugin-ai": "workspace:*" + "@cloudbeaver/plugin-data-grid": "workspace:*" "@cloudbeaver/tsconfig": "workspace:*" + "@dbeaver/ui-kit": "workspace:*" "@types/react": "npm:^19" mobx: "npm:^6" mobx-react-lite: "npm:^4" @@ -2607,6 +2607,7 @@ __metadata: rimraf: "npm:^6" tslib: "npm:^2" typescript: "npm:^5" + typescript-plugin-css-modules: "npm:^5" languageName: unknown linkType: soft From 3a89e8c4ec4197cdf6ddd7a0d4539852cb420d55 Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Thu, 3 Sep 2026 19:02:34 +0200 Subject: [PATCH 08/31] dbeaver/pro#9532 adds radio group label styles for scope --- .../src/FormControls/RadioGroup.module.css | 16 +++++++++ .../src/FormControls/RadioGroup.tsx | 36 +++++++++++++++++-- .../Options/AIProfileOptions.tsx | 13 +++---- .../src/locales/de.ts | 6 ++-- .../src/locales/en.ts | 6 ++-- .../src/locales/fr.ts | 6 ++-- .../src/locales/ru.ts | 6 ++-- .../src/components/AIUserProfilesTable.tsx | 6 ++-- .../plugin-ai-user-profile/src/locales/en.ts | 8 ++--- .../plugin-ai-user-profile/src/locales/fr.ts | 8 ++--- .../plugin-ai-user-profile/src/locales/ru.ts | 8 ++--- .../plugin-ai-user-profile/src/locales/zh.ts | 8 ++--- 12 files changed, 84 insertions(+), 43 deletions(-) create mode 100644 webapp/packages/core-blocks/src/FormControls/RadioGroup.module.css diff --git a/webapp/packages/core-blocks/src/FormControls/RadioGroup.module.css b/webapp/packages/core-blocks/src/FormControls/RadioGroup.module.css new file mode 100644 index 00000000000..1624032fbeb --- /dev/null +++ b/webapp/packages/core-blocks/src/FormControls/RadioGroup.module.css @@ -0,0 +1,16 @@ +/* + * 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. + */ + +.fieldLabel { + display: block; + font-weight: 500; +} + +.fieldLabel:not(:empty) { + padding-bottom: 10px; +} diff --git a/webapp/packages/core-blocks/src/FormControls/RadioGroup.tsx b/webapp/packages/core-blocks/src/FormControls/RadioGroup.tsx index 87b2796785d..c7b59e447f8 100644 --- a/webapp/packages/core-blocks/src/FormControls/RadioGroup.tsx +++ b/webapp/packages/core-blocks/src/FormControls/RadioGroup.tsx @@ -6,10 +6,15 @@ * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { useCallback, useContext, useState } from 'react'; +import { useCallback, useContext, useId, useState } from 'react'; +import { s } from '../s.js'; +import { useS } from '../useS.js'; +import { Field } from './Field.js'; +import { FieldLabel } from './FieldLabel.js'; import { FormContext } from './FormContext.js'; import { RadioGroup as UiKitRadioGroup } from '@dbeaver/ui-kit'; +import styles from './RadioGroup.module.css'; type BaseProps = React.PropsWithChildren> & { name: string; @@ -41,9 +46,15 @@ export const RadioGroup: RadioGroupType = observer(function RadioGroup({ state, onChange, children, + label, + labelledBy, + 'aria-label': ariaLabel, + required, ...rest }: ControlledProps | ObjectProps) { const formContext = useContext(FormContext); + const labelId = useId(); + const style = useS(styles); const [selfValue, setValue] = useState(); const handleChange = useCallback( @@ -71,8 +82,29 @@ export const RadioGroup: RadioGroupType = observer(function RadioGroup({ const value = state ? state[name] : (controlledValue ?? selfValue); + if (label) { + return ( + + + {label} + + + {children} + + + ); + } + + if (labelledBy) { + return ( + + {children} + + ); + } + return ( - + {children} ); diff --git a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfileOptions.tsx b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfileOptions.tsx index f7d97e7e38b..ce8a7fd6f1e 100644 --- a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfileOptions.tsx +++ b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfileOptions.tsx @@ -155,7 +155,7 @@ export const AIProfileOptions: TabContainerPanelComponent = setModels(null); } - function handleProfileTypeChange(value: string): void { + function handleScopeChange(value: string): void { const global = value === 'global'; part.state.global = global; part.state.properties['global'] = global; @@ -186,13 +186,14 @@ export const AIProfileOptions: TabContainerPanelComponent = {translate('plugin_ai_administration_profile_form_field_engine')} - {translate('plugin_ai_administration_profile_global_credentials')} + {translate('plugin_ai_administration_profile_scope_global')} = small keepSize > - {translate('plugin_ai_administration_profile_user_credentials')} + {translate('plugin_ai_administration_profile_scope_user')}
diff --git a/webapp/packages/plugin-ai-profiles-administration/src/locales/de.ts b/webapp/packages/plugin-ai-profiles-administration/src/locales/de.ts index d2e8e1d0a0d..6b03a02582d 100644 --- a/webapp/packages/plugin-ai-profiles-administration/src/locales/de.ts +++ b/webapp/packages/plugin-ai-profiles-administration/src/locales/de.ts @@ -24,9 +24,9 @@ export default [ ['plugin_ai_administration_profile_save_error', 'Profil konnte nicht gespeichert werden'], ['plugin_ai_administration_profile_form_field_name', 'Profilname'], ['plugin_ai_administration_profile_form_field_engine', 'Engine'], - ['plugin_ai_administration_profile_profile_type', 'Quelle der Anmeldedaten'], - ['plugin_ai_administration_profile_global_credentials', 'Globale Anmeldedaten'], - ['plugin_ai_administration_profile_user_credentials', 'Benutzeranmeldedaten'], + ['plugin_ai_administration_profile_scope', 'Geltungsbereich'], + ['plugin_ai_administration_profile_scope_global', 'Global'], + ['plugin_ai_administration_profile_scope_user', 'Benutzer'], ['plugin_ai_administration_profile_user_credentials_unsupported', 'Diese Engine unterstützt keine vom Benutzer bereitgestellten API-Token'], ['plugin_ai_administration_profile_form_tab_options', 'Profil'], ['plugin_ai_administration_profile_name_max_length', 'Der Profilname darf {arg:length} Zeichen nicht überschreiten'], diff --git a/webapp/packages/plugin-ai-profiles-administration/src/locales/en.ts b/webapp/packages/plugin-ai-profiles-administration/src/locales/en.ts index 868cd450801..0dc85990f30 100644 --- a/webapp/packages/plugin-ai-profiles-administration/src/locales/en.ts +++ b/webapp/packages/plugin-ai-profiles-administration/src/locales/en.ts @@ -24,9 +24,9 @@ export default [ ['plugin_ai_administration_profile_save_error', 'Failed to save profile'], ['plugin_ai_administration_profile_form_field_name', 'Profile name'], ['plugin_ai_administration_profile_form_field_engine', 'Engine'], - ['plugin_ai_administration_profile_profile_type', 'Credential source'], - ['plugin_ai_administration_profile_global_credentials', 'Global credentials'], - ['plugin_ai_administration_profile_user_credentials', 'User credentials'], + ['plugin_ai_administration_profile_scope', 'Scope'], + ['plugin_ai_administration_profile_scope_global', 'Global'], + ['plugin_ai_administration_profile_scope_user', 'User'], ['plugin_ai_administration_profile_user_credentials_unsupported', 'This engine does not support user-provided API tokens'], ['plugin_ai_administration_profile_form_tab_options', 'Profile'], ['plugin_ai_administration_profile_name_max_length', 'Profile name must not exceed {arg:length} characters'], diff --git a/webapp/packages/plugin-ai-profiles-administration/src/locales/fr.ts b/webapp/packages/plugin-ai-profiles-administration/src/locales/fr.ts index 4c85a67745f..0ea256182f8 100644 --- a/webapp/packages/plugin-ai-profiles-administration/src/locales/fr.ts +++ b/webapp/packages/plugin-ai-profiles-administration/src/locales/fr.ts @@ -24,9 +24,9 @@ export default [ ['plugin_ai_administration_profile_save_error', 'Échec de l’enregistrement du profil'], ['plugin_ai_administration_profile_form_field_name', 'Nom du profil'], ['plugin_ai_administration_profile_form_field_engine', "Modèle d'IA"], - ['plugin_ai_administration_profile_profile_type', 'Source des identifiants'], - ['plugin_ai_administration_profile_global_credentials', 'Identifiants globaux'], - ['plugin_ai_administration_profile_user_credentials', 'Identifiants utilisateur'], + ['plugin_ai_administration_profile_scope', 'Portée'], + ['plugin_ai_administration_profile_scope_global', 'Globale'], + ['plugin_ai_administration_profile_scope_user', 'Utilisateur'], ['plugin_ai_administration_profile_user_credentials_unsupported', 'Ce moteur ne prend pas en charge les jetons API fournis par les utilisateurs'], ['plugin_ai_administration_profile_form_tab_options', 'Profil'], ['plugin_ai_administration_profile_name_max_length', 'Le nom du profil ne doit pas dépasser {arg:length} caractères'], diff --git a/webapp/packages/plugin-ai-profiles-administration/src/locales/ru.ts b/webapp/packages/plugin-ai-profiles-administration/src/locales/ru.ts index f48383d8a17..f0c8ed62ee6 100644 --- a/webapp/packages/plugin-ai-profiles-administration/src/locales/ru.ts +++ b/webapp/packages/plugin-ai-profiles-administration/src/locales/ru.ts @@ -24,9 +24,9 @@ export default [ ['plugin_ai_administration_profile_save_error', 'Не удалось сохранить профиль'], ['plugin_ai_administration_profile_form_field_name', 'Название профиля'], ['plugin_ai_administration_profile_form_field_engine', 'Энджин'], - ['plugin_ai_administration_profile_profile_type', 'Источник учетных данных'], - ['plugin_ai_administration_profile_global_credentials', 'Глобальные учетные данные'], - ['plugin_ai_administration_profile_user_credentials', 'Учетные данные пользователя'], + ['plugin_ai_administration_profile_scope', 'Область'], + ['plugin_ai_administration_profile_scope_global', 'Глобальная'], + ['plugin_ai_administration_profile_scope_user', 'Пользовательская'], ['plugin_ai_administration_profile_user_credentials_unsupported', 'Этот движок не поддерживает API-токены, предоставляемые пользователями'], ['plugin_ai_administration_profile_form_tab_options', 'Профиль'], ['plugin_ai_administration_profile_name_max_length', 'Название профиля не должно превышать {arg:length} символов'], diff --git a/webapp/packages/plugin-ai-user-profile/src/components/AIUserProfilesTable.tsx b/webapp/packages/plugin-ai-user-profile/src/components/AIUserProfilesTable.tsx index 5b1da2423ef..6f915ecb865 100644 --- a/webapp/packages/plugin-ai-user-profile/src/components/AIUserProfilesTable.tsx +++ b/webapp/packages/plugin-ai-user-profile/src/components/AIUserProfilesTable.tsx @@ -37,11 +37,11 @@ export const AIUserProfilesTable = observer(function AIUserProfilesTable( const columns: IAIProfilesTableColumn[] = [ { - key: 'credentialSource', - label: 'plugin_ai_user_profile_column_credential_source', + key: 'scope', + label: 'plugin_ai_user_profile_column_scope', width: 180, render: profile => - translate(profile.global ? 'plugin_ai_user_profile_credential_source_administrator' : 'plugin_ai_user_profile_credential_source_user'), + translate(profile.global ? 'plugin_ai_user_profile_scope_global' : 'plugin_ai_user_profile_scope_user'), }, { key: 'status', diff --git a/webapp/packages/plugin-ai-user-profile/src/locales/en.ts b/webapp/packages/plugin-ai-user-profile/src/locales/en.ts index fe1d3f3f77c..1ab98728de7 100644 --- a/webapp/packages/plugin-ai-user-profile/src/locales/en.ts +++ b/webapp/packages/plugin-ai-user-profile/src/locales/en.ts @@ -12,14 +12,12 @@ export default [ ['plugin_ai_user_profile_empty', 'No AI profiles are available'], ['plugin_ai_user_profile_column_profile', 'Profile'], ['plugin_ai_user_profile_column_engine', 'Engine'], - ['plugin_ai_user_profile_column_credential_source', 'Credential source'], + ['plugin_ai_user_profile_column_scope', 'Scope'], ['plugin_ai_user_profile_column_status', 'Status'], - ['plugin_ai_user_profile_credential_source_administrator', 'Administrator'], - ['plugin_ai_user_profile_credential_source_user', 'User'], + ['plugin_ai_user_profile_scope_global', 'Global'], + ['plugin_ai_user_profile_scope_user', 'User'], ['plugin_ai_user_profile_status_managed', 'Managed by administrator'], ['plugin_ai_user_profile_status_configured', 'Configured'], ['plugin_ai_user_profile_status_not_configured', 'Not configured'], - ['plugin_ai_user_profile_action_configure_credentials', 'Configure credentials'], - ['plugin_ai_user_profile_action_edit_credentials', 'Edit credentials'], ['plugin_ai_user_profile_credentials_edit_failed', 'Failed to edit AI profile credentials'], ]; diff --git a/webapp/packages/plugin-ai-user-profile/src/locales/fr.ts b/webapp/packages/plugin-ai-user-profile/src/locales/fr.ts index 3ee6da699d7..6eb039e21ea 100644 --- a/webapp/packages/plugin-ai-user-profile/src/locales/fr.ts +++ b/webapp/packages/plugin-ai-user-profile/src/locales/fr.ts @@ -12,14 +12,12 @@ export default [ ['plugin_ai_user_profile_empty', "Aucun profil IA n'est disponible"], ['plugin_ai_user_profile_column_profile', 'Profil'], ['plugin_ai_user_profile_column_engine', 'Moteur'], - ['plugin_ai_user_profile_column_credential_source', 'Source des identifiants'], + ['plugin_ai_user_profile_column_scope', 'Portée'], ['plugin_ai_user_profile_column_status', 'Statut'], - ['plugin_ai_user_profile_credential_source_administrator', 'Administrateur'], - ['plugin_ai_user_profile_credential_source_user', 'Utilisateur'], + ['plugin_ai_user_profile_scope_global', 'Globale'], + ['plugin_ai_user_profile_scope_user', 'Utilisateur'], ['plugin_ai_user_profile_status_managed', "Géré par l'administrateur"], ['plugin_ai_user_profile_status_configured', 'Configuré'], ['plugin_ai_user_profile_status_not_configured', 'Non configuré'], - ['plugin_ai_user_profile_action_configure_credentials', 'Configurer les identifiants'], - ['plugin_ai_user_profile_action_edit_credentials', 'Modifier les identifiants'], ['plugin_ai_user_profile_credentials_edit_failed', 'Échec de la modification des identifiants du profil IA'], ]; diff --git a/webapp/packages/plugin-ai-user-profile/src/locales/ru.ts b/webapp/packages/plugin-ai-user-profile/src/locales/ru.ts index 06bf028a1c1..5b775e8d5c8 100644 --- a/webapp/packages/plugin-ai-user-profile/src/locales/ru.ts +++ b/webapp/packages/plugin-ai-user-profile/src/locales/ru.ts @@ -12,14 +12,12 @@ export default [ ['plugin_ai_user_profile_empty', 'Нет доступных профилей ИИ'], ['plugin_ai_user_profile_column_profile', 'Профиль'], ['plugin_ai_user_profile_column_engine', 'Движок'], - ['plugin_ai_user_profile_column_credential_source', 'Источник учетных данных'], + ['plugin_ai_user_profile_column_scope', 'Область'], ['plugin_ai_user_profile_column_status', 'Статус'], - ['plugin_ai_user_profile_credential_source_administrator', 'Администратор'], - ['plugin_ai_user_profile_credential_source_user', 'Пользователь'], + ['plugin_ai_user_profile_scope_global', 'Глобальная'], + ['plugin_ai_user_profile_scope_user', 'Пользовательская'], ['plugin_ai_user_profile_status_managed', 'Управляется администратором'], ['plugin_ai_user_profile_status_configured', 'Настроено'], ['plugin_ai_user_profile_status_not_configured', 'Не настроено'], - ['plugin_ai_user_profile_action_configure_credentials', 'Настроить учетные данные'], - ['plugin_ai_user_profile_action_edit_credentials', 'Изменить учетные данные'], ['plugin_ai_user_profile_credentials_edit_failed', 'Не удалось изменить учетные данные профиля ИИ'], ]; diff --git a/webapp/packages/plugin-ai-user-profile/src/locales/zh.ts b/webapp/packages/plugin-ai-user-profile/src/locales/zh.ts index 761564e3785..593415d5903 100644 --- a/webapp/packages/plugin-ai-user-profile/src/locales/zh.ts +++ b/webapp/packages/plugin-ai-user-profile/src/locales/zh.ts @@ -12,14 +12,12 @@ export default [ ['plugin_ai_user_profile_empty', '没有可用的 AI 配置文件'], ['plugin_ai_user_profile_column_profile', '配置文件'], ['plugin_ai_user_profile_column_engine', '引擎'], - ['plugin_ai_user_profile_column_credential_source', '凭据来源'], + ['plugin_ai_user_profile_column_scope', '范围'], ['plugin_ai_user_profile_column_status', '状态'], - ['plugin_ai_user_profile_credential_source_administrator', '管理员'], - ['plugin_ai_user_profile_credential_source_user', '用户'], + ['plugin_ai_user_profile_scope_global', '全局'], + ['plugin_ai_user_profile_scope_user', '用户'], ['plugin_ai_user_profile_status_managed', '由管理员管理'], ['plugin_ai_user_profile_status_configured', '已配置'], ['plugin_ai_user_profile_status_not_configured', '未配置'], - ['plugin_ai_user_profile_action_configure_credentials', '配置凭据'], - ['plugin_ai_user_profile_action_edit_credentials', '编辑凭据'], ['plugin_ai_user_profile_credentials_edit_failed', '无法编辑 AI 配置文件凭据'], ]; From 8e4bb95f6a4e1345cc31050a97a880a5dd1fcd9f Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Thu, 3 Sep 2026 19:17:00 +0200 Subject: [PATCH 09/31] dbeaver/pro#9532 fixes width blink for creds dialog --- .../plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx index 962ebfb5b88..728b61c4690 100644 --- a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx +++ b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx @@ -107,7 +107,7 @@ export const AIProfileCredentialsDialog: DialogComponent - + Date: Thu, 3 Sep 2026 20:58:39 +0200 Subject: [PATCH 10/31] dbeaver/pro#9532 add slide panel + form flow for user scope profile credentials --- .../src/AIProfileCredentialsDialog.tsx | 8 + .../plugin-ai-profiles/src/locales/en.ts | 2 + .../plugin-ai-profiles/src/locales/fr.ts | 2 + .../plugin-ai-profiles/src/locales/ru.ts | 2 + .../plugin-ai-profiles/src/locales/zh.ts | 2 + .../plugin-ai-user-profile/package.json | 5 + .../AIProfileCredentialsFormPart.ts | 71 +++++++ .../IAIProfileCredentialsFormState.ts | 11 + .../getAIProfileCredentialsFormPart.ts | 24 +++ .../src/AIProfileCredentialsPanelService.ts | 95 +++++++++ .../src/AIUserProfileBootstrap.ts | 4 +- .../src/AI_PROFILES_TAB_ID.ts | 9 + .../components/AIProfileCredentialsPanel.tsx | 198 ++++++++++++++++++ .../src/components/AIProfilesPanel.tsx | 13 +- .../src/components/AIUserProfilesTable.tsx | 13 +- .../plugin-ai-user-profile/src/locales/en.ts | 2 + .../plugin-ai-user-profile/src/locales/fr.ts | 2 + .../plugin-ai-user-profile/src/locales/ru.ts | 2 + .../plugin-ai-user-profile/src/locales/zh.ts | 2 + .../plugin-ai-user-profile/src/module.ts | 2 + .../plugin-ai-user-profile/tsconfig.json | 15 ++ webapp/yarn.lock | 5 + 22 files changed, 477 insertions(+), 12 deletions(-) create mode 100644 webapp/packages/plugin-ai-user-profile/src/AIProfileCredentialsForm/AIProfileCredentialsFormPart.ts create mode 100644 webapp/packages/plugin-ai-user-profile/src/AIProfileCredentialsForm/IAIProfileCredentialsFormState.ts create mode 100644 webapp/packages/plugin-ai-user-profile/src/AIProfileCredentialsForm/getAIProfileCredentialsFormPart.ts create mode 100644 webapp/packages/plugin-ai-user-profile/src/AIProfileCredentialsPanelService.ts create mode 100644 webapp/packages/plugin-ai-user-profile/src/AI_PROFILES_TAB_ID.ts create mode 100644 webapp/packages/plugin-ai-user-profile/src/components/AIProfileCredentialsPanel.tsx diff --git a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx index 728b61c4690..260a2a25148 100644 --- a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx +++ b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx @@ -69,6 +69,10 @@ export const AIProfileCredentialsDialog: DialogComponent ({ + profileName: '', + engineName: '', + credentialsSaved: false, + token: '', +}); + +export class AIProfileCredentialsFormPart extends FormPart { + constructor( + formState: IFormState, + private readonly aiProfilesResource: AIProfilesResource, + private readonly aiEnginesResource: AiEnginesResource, + ) { + super(formState, getDefaultState()); + } + + async resetCredentials(): Promise { + try { + this.isSaving = true; + const reset = await this.aiProfilesResource.resetCredentials(this.formState.state.profileId); + if (!reset) { + throw new Error('plugin_ai_credentials_reset_failed'); + } + + await this.reload(); + } finally { + this.isSaving = false; + } + } + + protected override async loader(): Promise { + const [profile] = await Promise.all([this.aiProfilesResource.load(this.formState.state.profileId), this.aiEnginesResource.load()]); + + if (!profile) { + throw new Error('plugin_ai_credentials_profile_not_found'); + } + + const engine = this.aiEnginesResource.data.find(engine => engine.id === profile.engineId); + this.setInitialState({ + profileName: profile.name, + engineName: engine?.name ?? profile.engineId, + credentialsSaved: profile.credentialsSaved, + token: '', + }); + } + + protected override async saveChanges(): Promise { + await this.aiProfilesResource.saveCredentials(this.formState.state.profileId, this.state.token); + } +} diff --git a/webapp/packages/plugin-ai-user-profile/src/AIProfileCredentialsForm/IAIProfileCredentialsFormState.ts b/webapp/packages/plugin-ai-user-profile/src/AIProfileCredentialsForm/IAIProfileCredentialsFormState.ts new file mode 100644 index 00000000000..f3b73e0d72f --- /dev/null +++ b/webapp/packages/plugin-ai-user-profile/src/AIProfileCredentialsForm/IAIProfileCredentialsFormState.ts @@ -0,0 +1,11 @@ +/* + * 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 interface IAIProfileCredentialsFormState { + profileId: string; +} diff --git a/webapp/packages/plugin-ai-user-profile/src/AIProfileCredentialsForm/getAIProfileCredentialsFormPart.ts b/webapp/packages/plugin-ai-user-profile/src/AIProfileCredentialsForm/getAIProfileCredentialsFormPart.ts new file mode 100644 index 00000000000..fd50a72a116 --- /dev/null +++ b/webapp/packages/plugin-ai-user-profile/src/AIProfileCredentialsForm/getAIProfileCredentialsFormPart.ts @@ -0,0 +1,24 @@ +/* + * 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 { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; +import type { IFormState } from '@cloudbeaver/core-ui'; +import { AiEnginesResource } from '@cloudbeaver/plugin-ai'; +import { AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; + +import { AIProfileCredentialsFormPart } from './AIProfileCredentialsFormPart.js'; +import type { IAIProfileCredentialsFormState } from './IAIProfileCredentialsFormState.js'; + +const DATA_CONTEXT_AI_PROFILE_CREDENTIALS_FORM_PART = createDataContext('ai-profile-credentials-form-part'); + +export function getAIProfileCredentialsFormPart(formState: IFormState): AIProfileCredentialsFormPart { + return formState.getPart(DATA_CONTEXT_AI_PROFILE_CREDENTIALS_FORM_PART, context => { + const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; + return new AIProfileCredentialsFormPart(formState, di.getService(AIProfilesResource), di.getService(AiEnginesResource)); + }); +} diff --git a/webapp/packages/plugin-ai-user-profile/src/AIProfileCredentialsPanelService.ts b/webapp/packages/plugin-ai-user-profile/src/AIProfileCredentialsPanelService.ts new file mode 100644 index 00000000000..22fd5d96a72 --- /dev/null +++ b/webapp/packages/plugin-ai-user-profile/src/AIProfileCredentialsPanelService.ts @@ -0,0 +1,95 @@ +/* + * 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 { 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 { NotificationService } from '@cloudbeaver/core-events'; +import { ExecutorInterrupter, type IExecutorHandler } from '@cloudbeaver/core-executor'; +import { LocalizationService } from '@cloudbeaver/core-localization'; +import { FormBaseService, FormMode, FormState, OptionsPanelService, type OptionsPanelCloseEventData } from '@cloudbeaver/core-ui'; +import { UserProfileTabsService } from '@cloudbeaver/plugin-user-profile'; + +import type { IAIProfileCredentialsFormState } from './AIProfileCredentialsForm/IAIProfileCredentialsFormState.js'; +import { AI_PROFILES_TAB_ID } from './AI_PROFILES_TAB_ID.js'; + +const AIProfileCredentialsPanel = importLazyComponent(() => + import('./components/AIProfileCredentialsPanel.js').then(module => module.AIProfileCredentialsPanel), +); +const panelGetter = () => AIProfileCredentialsPanel; + +@injectable(() => [LocalizationService, NotificationService, OptionsPanelService, UserProfileTabsService, IServiceProvider, CommonDialogService]) +export class AIProfileCredentialsPanelService extends FormBaseService { + formState: FormState | null = null; + + constructor( + localizationService: LocalizationService, + notificationService: NotificationService, + private readonly optionsPanelService: OptionsPanelService, + private readonly userProfileTabsService: UserProfileTabsService, + private readonly serviceProvider: IServiceProvider, + private readonly commonDialogService: CommonDialogService, + ) { + super(localizationService, notificationService, 'AIProfileCredentialsForm'); + + this.optionsPanelService.closeTask.addHandler(this.closeHandler); + + makeObservable(this, { + formState: observable.shallow, + open: action.bound, + close: action.bound, + }); + } + + async open(profileId: string): Promise { + const opened = await this.optionsPanelService.open(panelGetter); + + if (opened) { + await this.formState?.dispose(); + this.formState = new FormState(this.serviceProvider, this, { profileId }).setMode(FormMode.Edit); + } + + return opened; + } + + back(): Promise { + return this.userProfileTabsService.open(AI_PROFILES_TAB_ID); + } + + close(): Promise { + return this.optionsPanelService.close(); + } + + private readonly closeHandler: IExecutorHandler = async (data, contexts) => { + if (!this.optionsPanelService.isOpen(panelGetter)) { + return; + } + + if (data === 'after') { + await this.formState?.dispose(); + this.formState = null; + 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; + } + } + }; +} diff --git a/webapp/packages/plugin-ai-user-profile/src/AIUserProfileBootstrap.ts b/webapp/packages/plugin-ai-user-profile/src/AIUserProfileBootstrap.ts index 98ded1ac7f2..49611c58bdb 100644 --- a/webapp/packages/plugin-ai-user-profile/src/AIUserProfileBootstrap.ts +++ b/webapp/packages/plugin-ai-user-profile/src/AIUserProfileBootstrap.ts @@ -13,9 +13,9 @@ import { FEATURE_AI_ID, ServerConfigResource } from '@cloudbeaver/core-root'; import { AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; import { UserProfileTabsService } from '@cloudbeaver/plugin-user-profile'; -const AIProfilesPanel = importLazyComponent(() => import('./components/AIProfilesPanel.js').then(module => module.AIProfilesPanel)); +import { AI_PROFILES_TAB_ID } from './AI_PROFILES_TAB_ID.js'; -const AI_PROFILES_TAB_ID = 'ai_profiles'; +const AIProfilesPanel = importLazyComponent(() => import('./components/AIProfilesPanel.js').then(module => module.AIProfilesPanel)); @injectable(() => [UserProfileTabsService, AppAuthService, ServerConfigResource, AIProfilesResource]) export class AIUserProfileBootstrap extends Bootstrap { diff --git a/webapp/packages/plugin-ai-user-profile/src/AI_PROFILES_TAB_ID.ts b/webapp/packages/plugin-ai-user-profile/src/AI_PROFILES_TAB_ID.ts new file mode 100644 index 00000000000..10a0ac3da9e --- /dev/null +++ b/webapp/packages/plugin-ai-user-profile/src/AI_PROFILES_TAB_ID.ts @@ -0,0 +1,9 @@ +/* + * 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 const AI_PROFILES_TAB_ID = 'ai_profiles'; diff --git a/webapp/packages/plugin-ai-user-profile/src/components/AIProfileCredentialsPanel.tsx b/webapp/packages/plugin-ai-user-profile/src/components/AIProfileCredentialsPanel.tsx new file mode 100644 index 00000000000..56ddbc67c8d --- /dev/null +++ b/webapp/packages/plugin-ai-user-profile/src/components/AIProfileCredentialsPanel.tsx @@ -0,0 +1,198 @@ +/* + * 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 { + Button, + ColoredContainer, + ConfirmationDialog, + Container, + Form, + Group, + GroupBack, + GroupTitle, + InputField, + SAVED_VALUE_INDICATOR, + StatusMessage, + Text, + useAutoLoad, + useFocus, + useForm, + useTranslate, +} from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { ENotificationType, NotificationService } from '@cloudbeaver/core-events'; +import { type IFormState, TabList, type TabContainerPanelComponent, TabPanelList, TabsContainer, TabsState } from '@cloudbeaver/core-ui'; +import { getFirstException } from '@cloudbeaver/core-utils'; + +import { getAIProfileCredentialsFormPart } from '../AIProfileCredentialsForm/getAIProfileCredentialsFormPart.js'; +import type { IAIProfileCredentialsFormState } from '../AIProfileCredentialsForm/IAIProfileCredentialsFormState.js'; +import { AIProfileCredentialsPanelService } from '../AIProfileCredentialsPanelService.js'; + +const CREDENTIALS_TAB_ID = 'credentials'; +interface IAIProfileCredentialsFieldsProps { + formState: IFormState; +} + +const tabs = new TabsContainer('AI Profile Credentials'); + +tabs.add({ + key: CREDENTIALS_TAB_ID, + name: 'plugin_ai_credentials_dialog_title', + panel: () => AIProfileCredentialsFields, +}); + +const AIProfileCredentialsFields: TabContainerPanelComponent = observer(function AIProfileCredentialsFields({ + formState, +}) { + const translate = useTranslate(); + const notificationService = useService(NotificationService); + const commonDialogService = useService(CommonDialogService); + const [tokenRef] = useFocus({ autofocus: true }); + const part = getAIProfileCredentialsFormPart(formState); + const state = part.state; + + useAutoLoad(AIProfileCredentialsFields, part); + + async function resetCredentials(): Promise { + if (!state) { + return; + } + + const { status } = await commonDialogService.open(ConfirmationDialog, { + title: translate('plugin_ai_credentials_reset_title'), + message: 'plugin_ai_credentials_reset_confirmation', + confirmActionText: 'plugin_ai_credentials_reset', + }); + + if (status !== DialogueStateResult.Resolved) { + return; + } + + try { + await part.resetCredentials(); + notificationService.logSuccess({ + title: 'plugin_ai_user_profile_credentials_reset', + message: state.profileName, + }); + } catch (exception: any) { + notificationService.logException(exception, 'plugin_ai_credentials_reset_failed'); + } + } + + return ( + + {translate('plugin_ai_credentials_dialog_description')} + + + {translate('plugin_ai_credentials_profile')} + + + {translate('plugin_ai_credentials_engine')} + + + {translate('plugin_ai_credentials_token')} + + + {state.credentialsSaved && ( +
+ +
+ )} +
+ ); +}); + +export const AIProfileCredentialsPanel = observer(function AIProfileCredentialsPanel() { + const translate = useTranslate(); + const credentialsPanelService = useService(AIProfileCredentialsPanelService); + const notificationService = useService(NotificationService); + const formState = credentialsPanelService.formState; + const form = useForm({ onSubmit: save }); + + if (!formState) { + return null; + } + + const part = getAIProfileCredentialsFormPart(formState); + const title = `${translate('ui_edit')} "${part.state.profileName}"`; + + async function save(): Promise { + if (!formState) { + return; + } + + const saved = await formState.save(); + + if (saved) { + notificationService.logSuccess({ + title: 'plugin_ai_user_profile_credentials_saved', + message: part.state.profileName, + }); + await credentialsPanelService.back(); + return; + } + + const exception = getFirstException(formState.exception); + if (exception) { + notificationService.logException(exception, 'plugin_ai_credentials_save_failed'); + } + } + + return ( + + + credentialsPanelService.back()}> + {title} + + +
+ + + + + + + + + + + + + + + + + +
+
+ ); +}); diff --git a/webapp/packages/plugin-ai-user-profile/src/components/AIProfilesPanel.tsx b/webapp/packages/plugin-ai-user-profile/src/components/AIProfilesPanel.tsx index 25d095dac27..a47c3a0c29a 100644 --- a/webapp/packages/plugin-ai-user-profile/src/components/AIProfilesPanel.tsx +++ b/webapp/packages/plugin-ai-user-profile/src/components/AIProfilesPanel.tsx @@ -7,7 +7,16 @@ */ import { observer } from 'mobx-react-lite'; -import { ColoredContainer, Container, Group, TextPlaceholder, ToolsAction, ToolsPanel, useResource, useTranslate } from '@cloudbeaver/core-blocks'; +import { + ColoredContainer, + Container, + Group, + TextPlaceholder, + ToolsAction, + ToolsPanel, + useResource, + useTranslate, +} from '@cloudbeaver/core-blocks'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; @@ -32,7 +41,7 @@ export const AIProfilesPanel = observer(function AIProfilesPanel() { } return ( - + (function AIUserProfilesTable({ profiles }) { const translate = useTranslate(); - const credentialsService = useService(AIProfileCredentialsService); + const credentialsPanelService = useService(AIProfileCredentialsPanelService); const notificationService = useService(NotificationService); async function editCredentials(profileId: string): Promise { try { - await credentialsService.open(profileId); + await credentialsPanelService.open(profileId); } catch (exception: any) { notificationService.logException(exception, 'plugin_ai_user_profile_credentials_edit_failed'); } diff --git a/webapp/packages/plugin-ai-user-profile/src/locales/en.ts b/webapp/packages/plugin-ai-user-profile/src/locales/en.ts index 1ab98728de7..768a168e6d2 100644 --- a/webapp/packages/plugin-ai-user-profile/src/locales/en.ts +++ b/webapp/packages/plugin-ai-user-profile/src/locales/en.ts @@ -20,4 +20,6 @@ export default [ ['plugin_ai_user_profile_status_configured', 'Configured'], ['plugin_ai_user_profile_status_not_configured', 'Not configured'], ['plugin_ai_user_profile_credentials_edit_failed', 'Failed to edit AI profile credentials'], + ['plugin_ai_user_profile_credentials_saved', 'AI profile information saved'], + ['plugin_ai_user_profile_credentials_reset', 'AI profile credentials reset'], ]; diff --git a/webapp/packages/plugin-ai-user-profile/src/locales/fr.ts b/webapp/packages/plugin-ai-user-profile/src/locales/fr.ts index 6eb039e21ea..7650f942c3b 100644 --- a/webapp/packages/plugin-ai-user-profile/src/locales/fr.ts +++ b/webapp/packages/plugin-ai-user-profile/src/locales/fr.ts @@ -20,4 +20,6 @@ export default [ ['plugin_ai_user_profile_status_configured', 'Configuré'], ['plugin_ai_user_profile_status_not_configured', 'Non configuré'], ['plugin_ai_user_profile_credentials_edit_failed', 'Échec de la modification des identifiants du profil IA'], + ['plugin_ai_user_profile_credentials_saved', 'Informations du profil IA enregistrées'], + ['plugin_ai_user_profile_credentials_reset', 'Identifiants du profil IA réinitialisés'], ]; diff --git a/webapp/packages/plugin-ai-user-profile/src/locales/ru.ts b/webapp/packages/plugin-ai-user-profile/src/locales/ru.ts index 5b775e8d5c8..d9293e74b27 100644 --- a/webapp/packages/plugin-ai-user-profile/src/locales/ru.ts +++ b/webapp/packages/plugin-ai-user-profile/src/locales/ru.ts @@ -20,4 +20,6 @@ export default [ ['plugin_ai_user_profile_status_configured', 'Настроено'], ['plugin_ai_user_profile_status_not_configured', 'Не настроено'], ['plugin_ai_user_profile_credentials_edit_failed', 'Не удалось изменить учетные данные профиля ИИ'], + ['plugin_ai_user_profile_credentials_saved', 'Информация о профиле ИИ сохранена'], + ['plugin_ai_user_profile_credentials_reset', 'Учетные данные профиля ИИ сброшены'], ]; diff --git a/webapp/packages/plugin-ai-user-profile/src/locales/zh.ts b/webapp/packages/plugin-ai-user-profile/src/locales/zh.ts index 593415d5903..e873ef996be 100644 --- a/webapp/packages/plugin-ai-user-profile/src/locales/zh.ts +++ b/webapp/packages/plugin-ai-user-profile/src/locales/zh.ts @@ -20,4 +20,6 @@ export default [ ['plugin_ai_user_profile_status_configured', '已配置'], ['plugin_ai_user_profile_status_not_configured', '未配置'], ['plugin_ai_user_profile_credentials_edit_failed', '无法编辑 AI 配置文件凭据'], + ['plugin_ai_user_profile_credentials_saved', 'AI 配置文件信息已保存'], + ['plugin_ai_user_profile_credentials_reset', 'AI 配置文件凭据已重置'], ]; diff --git a/webapp/packages/plugin-ai-user-profile/src/module.ts b/webapp/packages/plugin-ai-user-profile/src/module.ts index bb6019284c9..b61bbf49016 100644 --- a/webapp/packages/plugin-ai-user-profile/src/module.ts +++ b/webapp/packages/plugin-ai-user-profile/src/module.ts @@ -9,6 +9,7 @@ import { Bootstrap, ModuleRegistry } from '@cloudbeaver/core-di'; import { AIUserProfileBootstrap } from './AIUserProfileBootstrap.js'; +import { AIProfileCredentialsPanelService } from './AIProfileCredentialsPanelService.js'; import { LocaleService } from './LocaleService.js'; export default ModuleRegistry.add({ @@ -16,6 +17,7 @@ export default ModuleRegistry.add({ configure: serviceCollection => { serviceCollection + .addSingleton(AIProfileCredentialsPanelService) .addSingleton(Bootstrap, LocaleService) .addSingleton(Bootstrap, AIUserProfileBootstrap); }, diff --git a/webapp/packages/plugin-ai-user-profile/tsconfig.json b/webapp/packages/plugin-ai-user-profile/tsconfig.json index 3cfc2d0a8e8..4e15590e148 100644 --- a/webapp/packages/plugin-ai-user-profile/tsconfig.json +++ b/webapp/packages/plugin-ai-user-profile/tsconfig.json @@ -16,12 +16,21 @@ { "path": "../core-cli" }, + { + "path": "../core-data-context" + }, { "path": "../core-di" }, + { + "path": "../core-dialogs" + }, { "path": "../core-events" }, + { + "path": "../core-executor" + }, { "path": "../core-localization" }, @@ -31,6 +40,12 @@ { "path": "../core-root" }, + { + "path": "../core-ui" + }, + { + "path": "../core-utils" + }, { "path": "../plugin-ai" }, diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 23843074828..76ef76bdcbc 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -2618,11 +2618,16 @@ __metadata: "@cloudbeaver/core-authentication": "workspace:*" "@cloudbeaver/core-blocks": "workspace:*" "@cloudbeaver/core-cli": "workspace:*" + "@cloudbeaver/core-data-context": "workspace:*" "@cloudbeaver/core-di": "workspace:*" + "@cloudbeaver/core-dialogs": "workspace:*" "@cloudbeaver/core-events": "workspace:*" + "@cloudbeaver/core-executor": "workspace:*" "@cloudbeaver/core-localization": "workspace:*" "@cloudbeaver/core-resource": "workspace:*" "@cloudbeaver/core-root": "workspace:*" + "@cloudbeaver/core-ui": "workspace:*" + "@cloudbeaver/core-utils": "workspace:*" "@cloudbeaver/plugin-ai": "workspace:*" "@cloudbeaver/plugin-ai-profiles": "workspace:*" "@cloudbeaver/plugin-user-profile": "workspace:*" From 3c8cee4153ea2ec30ea25cddae29a5af86621c54 Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Thu, 3 Sep 2026 21:20:49 +0200 Subject: [PATCH 11/31] dbeaver/pro#9532 fixes width issue with names --- .../@dbeaver/react-data-grid/src/DataGrid.tsx | 3 ++- .../src/DataGridHeaderCellContext.ts | 1 + .../plugin-ai-profiles/src/AIProfilesTable.tsx | 18 +++++++++--------- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/webapp/common-react/@dbeaver/react-data-grid/src/DataGrid.tsx b/webapp/common-react/@dbeaver/react-data-grid/src/DataGrid.tsx index 2d58ba0522d..804e59f91ae 100644 --- a/webapp/common-react/@dbeaver/react-data-grid/src/DataGrid.tsx +++ b/webapp/common-react/@dbeaver/react-data-grid/src/DataGrid.tsx @@ -78,6 +78,7 @@ export const DataGrid = forwardRef(function DataGrid { headerElement, getHeaderWidth, + getHeaderMinWidth, headerText, getHeaderOrder, getHeaderResizable, @@ -147,7 +148,7 @@ export const DataGrid = forwardRef(function DataGrid name: '', resizable: getHeaderResizable?.(i) ?? true, width, - minWidth: 26, + minWidth: getHeaderMinWidth?.(i) ?? 26, editable: row => getCellEditable?.(row.idx, i) ?? false, frozen: getHeaderPinned?.(i), renderHeaderCell: mapRenderHeaderCell(i), diff --git a/webapp/common-react/@dbeaver/react-data-grid/src/DataGridHeaderCellContext.ts b/webapp/common-react/@dbeaver/react-data-grid/src/DataGridHeaderCellContext.ts index c8028dc2813..2381526fca8 100644 --- a/webapp/common-react/@dbeaver/react-data-grid/src/DataGridHeaderCellContext.ts +++ b/webapp/common-react/@dbeaver/react-data-grid/src/DataGridHeaderCellContext.ts @@ -15,6 +15,7 @@ export interface IDataGridHeaderCellContext { headerText?: IGridReactiveValue; getHeaderOrder?: () => number[]; getHeaderWidth?: (colIdx: number) => number | string | null; + getHeaderMinWidth?: (colIdx: number) => number | null; getHeaderResizable?: (colIdx: number) => boolean; getHeaderHeight?: () => number; getHeaderPinned?: (colIdx: number) => boolean; diff --git a/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.tsx b/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.tsx index c100bc5d400..1678855072c 100644 --- a/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.tsx +++ b/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.tsx @@ -22,7 +22,8 @@ import AIProfilesTableStyles from './AIProfilesTable.module.css'; export interface IAIProfilesTableColumn { key: string; label: string; - width?: number; + width?: number | string; + minWidth?: number; render: (profile: AIProfile) => ReactNode; } @@ -43,7 +44,7 @@ interface Props { } const SELECT_COLUMN = { key: 'select', label: '' }; -const NAME_COLUMN = { key: 'name' }; +const NAME_COLUMN = { key: 'name', minWidth: 120 }; const ENGINE_COLUMN = { key: 'engine', width: 160 }; export const AIProfilesTable = observer(function AIProfilesTable({ @@ -85,13 +86,7 @@ export const AIProfilesTable = observer(function AIProfilesTable({ } if (column.key === SELECT_COLUMN.key) { - return ( - - ); + return ; } if (column.key === NAME_COLUMN.key) { @@ -181,6 +176,10 @@ export const AIProfilesTable = observer(function AIProfilesTable({ return columns[colIdx]?.width ?? null; } + function getHeaderMinWidth(colIdx: number) { + return columns[colIdx]?.minWidth ?? null; + } + return (
(function AIProfilesTable({ headerText={headerText} headerElement={headerElement} getHeaderWidth={getHeaderWidth} + getHeaderMinWidth={getHeaderMinWidth} cell={cell} className={s(styles, { table: true })} /> From 8aafb69e018951e6ef7cac733faf839c3f7dc415 Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Thu, 3 Sep 2026 22:01:50 +0200 Subject: [PATCH 12/31] dbeaver/pro#9532 adds scope column --- .../src/AIProfilesTable.tsx | 6 +++++ .../plugin-ai-profiles/src/LocaleService.ts | 2 ++ .../plugin-ai-profiles/src/locales/de.ts | 26 +++++++++++++++++++ .../plugin-ai-profiles/src/locales/en.ts | 3 +++ .../plugin-ai-profiles/src/locales/fr.ts | 3 +++ .../plugin-ai-profiles/src/locales/ru.ts | 3 +++ .../plugin-ai-profiles/src/locales/zh.ts | 3 +++ .../src/components/AIUserProfilesTable.tsx | 7 ----- .../plugin-ai-user-profile/src/locales/en.ts | 3 --- .../plugin-ai-user-profile/src/locales/fr.ts | 3 --- .../plugin-ai-user-profile/src/locales/ru.ts | 3 --- .../plugin-ai-user-profile/src/locales/zh.ts | 3 --- 12 files changed, 46 insertions(+), 19 deletions(-) create mode 100644 webapp/packages/plugin-ai-profiles/src/locales/de.ts diff --git a/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.tsx b/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.tsx index 1678855072c..a0ebb05d804 100644 --- a/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.tsx +++ b/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.tsx @@ -46,6 +46,7 @@ interface Props { const SELECT_COLUMN = { key: 'select', label: '' }; const NAME_COLUMN = { key: 'name', minWidth: 120 }; const ENGINE_COLUMN = { key: 'engine', width: 160 }; +const SCOPE_COLUMN = { key: 'scope', width: 120 }; export const AIProfilesTable = observer(function AIProfilesTable({ profiles, @@ -68,6 +69,11 @@ export const AIProfilesTable = observer(function AIProfilesTable({ ...(selectable ? [SELECT_COLUMN] : []), { ...NAME_COLUMN, label: nameLabel }, { ...ENGINE_COLUMN, label: engineLabel }, + { + ...SCOPE_COLUMN, + label: 'plugin_ai_profiles_scope', + render: profile => translate(profile.global ? 'plugin_ai_profiles_scope_global' : 'plugin_ai_profiles_scope_user'), + }, ...additionalColumns, ]; diff --git a/webapp/packages/plugin-ai-profiles/src/LocaleService.ts b/webapp/packages/plugin-ai-profiles/src/LocaleService.ts index 6d42886d2be..72176a379ab 100644 --- a/webapp/packages/plugin-ai-profiles/src/LocaleService.ts +++ b/webapp/packages/plugin-ai-profiles/src/LocaleService.ts @@ -27,6 +27,8 @@ export class LocaleService extends Bootstrap { return (await import('./locales/zh.js')).default; case 'fr': return (await import('./locales/fr.js')).default; + case 'de': + return (await import('./locales/de.js')).default; default: return (await import('./locales/en.js')).default; } diff --git a/webapp/packages/plugin-ai-profiles/src/locales/de.ts b/webapp/packages/plugin-ai-profiles/src/locales/de.ts new file mode 100644 index 00000000000..70ead7657d4 --- /dev/null +++ b/webapp/packages/plugin-ai-profiles/src/locales/de.ts @@ -0,0 +1,26 @@ +/* + * 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 default [ + ['plugin_ai_profiles_scope', 'Geltungsbereich'], + ['plugin_ai_profiles_scope_global', 'Global'], + ['plugin_ai_profiles_scope_user', 'Benutzer'], + ['plugin_ai_credentials_dialog_title', 'Anmeldedaten des KI-Profils'], + ['plugin_ai_credentials_dialog_description', 'Geben Sie die Anmeldedaten an, die dieses KI-Profil für Ihr Benutzerkonto verwendet.'], + ['plugin_ai_credentials_profile', 'Profil'], + ['plugin_ai_credentials_engine', 'Engine'], + ['plugin_ai_credentials_token', 'API-Token'], + ['plugin_ai_credentials_reset', 'Anmeldedaten zurücksetzen'], + ['plugin_ai_credentials_reset_title', 'Anmeldedaten des KI-Profils zurücksetzen'], + ['plugin_ai_credentials_reset_confirmation', 'Möchten Sie die gespeicherten Anmeldedaten wirklich zurücksetzen?'], + ['plugin_ai_credentials_profile_not_found', 'KI-Profil nicht gefunden'], + ['plugin_ai_credentials_saved', 'Anmeldedaten des KI-Profils gespeichert'], + ['plugin_ai_credentials_reset_success', 'Anmeldedaten des KI-Profils zurückgesetzt'], + ['plugin_ai_credentials_save_failed', 'Anmeldedaten des KI-Profils konnten nicht gespeichert werden'], + ['plugin_ai_credentials_reset_failed', 'Anmeldedaten des KI-Profils konnten nicht zurückgesetzt werden'], +]; diff --git a/webapp/packages/plugin-ai-profiles/src/locales/en.ts b/webapp/packages/plugin-ai-profiles/src/locales/en.ts index 498f46e3913..98a27a322c2 100644 --- a/webapp/packages/plugin-ai-profiles/src/locales/en.ts +++ b/webapp/packages/plugin-ai-profiles/src/locales/en.ts @@ -7,6 +7,9 @@ */ export default [ + ['plugin_ai_profiles_scope', 'Scope'], + ['plugin_ai_profiles_scope_global', 'Global'], + ['plugin_ai_profiles_scope_user', 'User'], ['plugin_ai_credentials_dialog_title', 'AI profile credentials'], ['plugin_ai_credentials_dialog_description', 'Provide the credentials used by this AI profile for your user account.'], ['plugin_ai_credentials_profile', 'Profile'], diff --git a/webapp/packages/plugin-ai-profiles/src/locales/fr.ts b/webapp/packages/plugin-ai-profiles/src/locales/fr.ts index a6767c96499..c9692ff3516 100644 --- a/webapp/packages/plugin-ai-profiles/src/locales/fr.ts +++ b/webapp/packages/plugin-ai-profiles/src/locales/fr.ts @@ -7,6 +7,9 @@ */ export default [ + ['plugin_ai_profiles_scope', 'Portée'], + ['plugin_ai_profiles_scope_global', 'Globale'], + ['plugin_ai_profiles_scope_user', 'Utilisateur'], ['plugin_ai_credentials_dialog_title', 'Identifiants du profil IA'], ['plugin_ai_credentials_dialog_description', 'Fournissez les identifiants utilisés par ce profil IA pour votre compte utilisateur.'], ['plugin_ai_credentials_profile', 'Profil'], diff --git a/webapp/packages/plugin-ai-profiles/src/locales/ru.ts b/webapp/packages/plugin-ai-profiles/src/locales/ru.ts index 92580060b97..e691cf23336 100644 --- a/webapp/packages/plugin-ai-profiles/src/locales/ru.ts +++ b/webapp/packages/plugin-ai-profiles/src/locales/ru.ts @@ -7,6 +7,9 @@ */ export default [ + ['plugin_ai_profiles_scope', 'Область'], + ['plugin_ai_profiles_scope_global', 'Глобальная'], + ['plugin_ai_profiles_scope_user', 'Пользовательская'], ['plugin_ai_credentials_dialog_title', 'Учетные данные профиля ИИ'], ['plugin_ai_credentials_dialog_description', 'Укажите учетные данные, которые этот профиль ИИ будет использовать для вашей учетной записи.'], ['plugin_ai_credentials_profile', 'Профиль'], diff --git a/webapp/packages/plugin-ai-profiles/src/locales/zh.ts b/webapp/packages/plugin-ai-profiles/src/locales/zh.ts index 44544ead073..b889cc58926 100644 --- a/webapp/packages/plugin-ai-profiles/src/locales/zh.ts +++ b/webapp/packages/plugin-ai-profiles/src/locales/zh.ts @@ -7,6 +7,9 @@ */ export default [ + ['plugin_ai_profiles_scope', '范围'], + ['plugin_ai_profiles_scope_global', '全局'], + ['plugin_ai_profiles_scope_user', '用户'], ['plugin_ai_credentials_dialog_title', 'AI 配置文件凭据'], ['plugin_ai_credentials_dialog_description', '提供此 AI 配置文件用于您的用户账户的凭据。'], ['plugin_ai_credentials_profile', '配置文件'], diff --git a/webapp/packages/plugin-ai-user-profile/src/components/AIUserProfilesTable.tsx b/webapp/packages/plugin-ai-user-profile/src/components/AIUserProfilesTable.tsx index a81ca764658..79d76bf5667 100644 --- a/webapp/packages/plugin-ai-user-profile/src/components/AIUserProfilesTable.tsx +++ b/webapp/packages/plugin-ai-user-profile/src/components/AIUserProfilesTable.tsx @@ -33,13 +33,6 @@ export const AIUserProfilesTable = observer(function AIUserProfilesTable( } const columns: IAIProfilesTableColumn[] = [ - { - key: 'scope', - label: 'plugin_ai_user_profile_column_scope', - width: 180, - render: profile => - translate(profile.global ? 'plugin_ai_user_profile_scope_global' : 'plugin_ai_user_profile_scope_user'), - }, { key: 'status', label: 'plugin_ai_user_profile_column_status', diff --git a/webapp/packages/plugin-ai-user-profile/src/locales/en.ts b/webapp/packages/plugin-ai-user-profile/src/locales/en.ts index 768a168e6d2..d59b61eb8f9 100644 --- a/webapp/packages/plugin-ai-user-profile/src/locales/en.ts +++ b/webapp/packages/plugin-ai-user-profile/src/locales/en.ts @@ -12,10 +12,7 @@ export default [ ['plugin_ai_user_profile_empty', 'No AI profiles are available'], ['plugin_ai_user_profile_column_profile', 'Profile'], ['plugin_ai_user_profile_column_engine', 'Engine'], - ['plugin_ai_user_profile_column_scope', 'Scope'], ['plugin_ai_user_profile_column_status', 'Status'], - ['plugin_ai_user_profile_scope_global', 'Global'], - ['plugin_ai_user_profile_scope_user', 'User'], ['plugin_ai_user_profile_status_managed', 'Managed by administrator'], ['plugin_ai_user_profile_status_configured', 'Configured'], ['plugin_ai_user_profile_status_not_configured', 'Not configured'], diff --git a/webapp/packages/plugin-ai-user-profile/src/locales/fr.ts b/webapp/packages/plugin-ai-user-profile/src/locales/fr.ts index 7650f942c3b..e31465efe7c 100644 --- a/webapp/packages/plugin-ai-user-profile/src/locales/fr.ts +++ b/webapp/packages/plugin-ai-user-profile/src/locales/fr.ts @@ -12,10 +12,7 @@ export default [ ['plugin_ai_user_profile_empty', "Aucun profil IA n'est disponible"], ['plugin_ai_user_profile_column_profile', 'Profil'], ['plugin_ai_user_profile_column_engine', 'Moteur'], - ['plugin_ai_user_profile_column_scope', 'Portée'], ['plugin_ai_user_profile_column_status', 'Statut'], - ['plugin_ai_user_profile_scope_global', 'Globale'], - ['plugin_ai_user_profile_scope_user', 'Utilisateur'], ['plugin_ai_user_profile_status_managed', "Géré par l'administrateur"], ['plugin_ai_user_profile_status_configured', 'Configuré'], ['plugin_ai_user_profile_status_not_configured', 'Non configuré'], diff --git a/webapp/packages/plugin-ai-user-profile/src/locales/ru.ts b/webapp/packages/plugin-ai-user-profile/src/locales/ru.ts index d9293e74b27..d24caa5ebc2 100644 --- a/webapp/packages/plugin-ai-user-profile/src/locales/ru.ts +++ b/webapp/packages/plugin-ai-user-profile/src/locales/ru.ts @@ -12,10 +12,7 @@ export default [ ['plugin_ai_user_profile_empty', 'Нет доступных профилей ИИ'], ['plugin_ai_user_profile_column_profile', 'Профиль'], ['plugin_ai_user_profile_column_engine', 'Движок'], - ['plugin_ai_user_profile_column_scope', 'Область'], ['plugin_ai_user_profile_column_status', 'Статус'], - ['plugin_ai_user_profile_scope_global', 'Глобальная'], - ['plugin_ai_user_profile_scope_user', 'Пользовательская'], ['plugin_ai_user_profile_status_managed', 'Управляется администратором'], ['plugin_ai_user_profile_status_configured', 'Настроено'], ['plugin_ai_user_profile_status_not_configured', 'Не настроено'], diff --git a/webapp/packages/plugin-ai-user-profile/src/locales/zh.ts b/webapp/packages/plugin-ai-user-profile/src/locales/zh.ts index e873ef996be..2db18858cc8 100644 --- a/webapp/packages/plugin-ai-user-profile/src/locales/zh.ts +++ b/webapp/packages/plugin-ai-user-profile/src/locales/zh.ts @@ -12,10 +12,7 @@ export default [ ['plugin_ai_user_profile_empty', '没有可用的 AI 配置文件'], ['plugin_ai_user_profile_column_profile', '配置文件'], ['plugin_ai_user_profile_column_engine', '引擎'], - ['plugin_ai_user_profile_column_scope', '范围'], ['plugin_ai_user_profile_column_status', '状态'], - ['plugin_ai_user_profile_scope_global', '全局'], - ['plugin_ai_user_profile_scope_user', '用户'], ['plugin_ai_user_profile_status_managed', '由管理员管理'], ['plugin_ai_user_profile_status_configured', '已配置'], ['plugin_ai_user_profile_status_not_configured', '未配置'], From 3d486ddbfd54be636b63578fcf51752ab051a3c5 Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Thu, 3 Sep 2026 23:05:44 +0200 Subject: [PATCH 13/31] dbeaver/pro#9532 outdates conversations for deleted profiles --- .../AIChatConversationsResource.ts | 22 +++++-------------- .../AIChatMessage/AIChatMessageService.ts | 16 ++------------ .../src/AIProfileCredentialsDialog.tsx | 2 +- .../src/AIProfilesTable.tsx | 4 ++-- .../src/AIProfilesTableLazy.ts | 11 ++++++++++ .../packages/plugin-ai-profiles/src/index.ts | 3 ++- 6 files changed, 24 insertions(+), 34 deletions(-) create mode 100644 webapp/packages/plugin-ai-profiles/src/AIProfilesTableLazy.ts diff --git a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationsResource.ts b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationsResource.ts index dcb44ba70ba..c4538c4494c 100644 --- a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationsResource.ts +++ b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationsResource.ts @@ -18,7 +18,6 @@ import { type ResourceKey, } from '@cloudbeaver/core-resource'; import { type AiChatConversationFragment, type AiChatConversationInput, GraphQLService } from '@cloudbeaver/core-sdk'; -import { AISettingsResource } from '@cloudbeaver/plugin-ai'; import { AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; import type { EAIConversationPromptGeneratorId } from '../../EAIConversationPromptGeneratorId.js'; @@ -34,13 +33,12 @@ export const ChatConversationConnectionKey = resourceKeyListAliasFactory( }), ); -@injectable(() => [GraphQLService, UserInfoResource, AIProfilesResource, AISettingsResource]) +@injectable(() => [GraphQLService, UserInfoResource, AIProfilesResource]) export class AIChatConversationsResource extends CachedMapResource { constructor( private readonly graphQLService: GraphQLService, userInfoResource: UserInfoResource, aiProfilesResource: AIProfilesResource, - aiSettingsResource: AISettingsResource, ) { super(); @@ -48,21 +46,13 @@ export class AIChatConversationsResource extends CachedMapResource { + aiProfilesResource.onItemDelete.addHandler(key => { const deletedProfileIds = ResourceKeyUtils.toArray(key); - const conversations = this.values.filter(conversation => conversation.profile && deletedProfileIds.includes(conversation.profile)); - if (conversations.length === 0) { - return; - } - - const defaultProfileId = (await aiSettingsResource.load())?.defaultConfiguration; - if (!defaultProfileId || deletedProfileIds.includes(defaultProfileId)) { - return; - } + const conversationIds = this.values + .filter(conversation => conversation.profile && deletedProfileIds.includes(conversation.profile)) + .map(conversation => conversation.id); - await Promise.all( - conversations.map(conversation => this.updateConversation(conversation.id, { settings: { profile: defaultProfileId } })), - ); + this.markOutdated(resourceKeyList(conversationIds)); }); this.aliases.add(ChatConversationConnectionKey, param => diff --git a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/AIChatMessageService.ts b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/AIChatMessageService.ts index 48e73cd7bf8..28058d82c61 100644 --- a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/AIChatMessageService.ts +++ b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/AIChatMessageService.ts @@ -15,7 +15,6 @@ import { LocalizationService } from '@cloudbeaver/core-localization'; import { Executor, ExecutorInterrupter } from '@cloudbeaver/core-executor'; import { ConnectionsManagerService, type IConnectionInfoParams } from '@cloudbeaver/core-connections'; import type { AiSendChatMessageInfoFragment } from '@cloudbeaver/core-sdk'; -import { AISettingsResource } from '@cloudbeaver/plugin-ai'; import { AIProfileCredentialsService, AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; import { AIChatMessagesResource, isFunctionConfirmationMessage, isFunctionMessage, type IMessageParam } from './AIChatMessagesResource.js'; @@ -53,7 +52,6 @@ type MessageSendExecutorData = IMessageSendExecutorBeforeData | IMessageSendExec LocalizationService, ConnectionsManagerService, AIProfilesResource, - AISettingsResource, AIProfileCredentialsService, ]) export class AIChatMessageService { @@ -66,7 +64,6 @@ export class AIChatMessageService { private readonly localizationService: LocalizationService, private readonly connectionsManagerService: ConnectionsManagerService, private readonly aiProfilesResource: AIProfilesResource, - private readonly aiSettingsResource: AISettingsResource, private readonly credentialsService: AIProfileCredentialsService, ) { this.onMessageSend = new Executor(); @@ -128,17 +125,8 @@ export class AIChatMessageService { async processSendMessageAction(conversationId: string, action: () => Promise) { const conversation = await this.aiChatConversationsResource.load(conversationId); - const settings = await this.aiSettingsResource.load(); - let profileId = conversation.profile ?? settings?.defaultConfiguration; - let profile = profileId ? await this.aiProfilesResource.load(profileId) : undefined; - - if (!profile && conversation.profile && settings?.defaultConfiguration && conversation.profile !== settings.defaultConfiguration) { - profileId = settings.defaultConfiguration; - profile = await this.aiProfilesResource.load(profileId); - if (profile) { - await this.aiChatConversationsResource.updateConversation(conversation.id, { settings: { profile: profileId } }); - } - } + const profileId = conversation.profile; + const profile = profileId ? await this.aiProfilesResource.load(profileId) : undefined; if (profileId) { if (profile && this.credentialsService.isRequired(profile)) { diff --git a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx index 260a2a25148..04fb9b668ff 100644 --- a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx +++ b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx @@ -120,7 +120,7 @@ export const AIProfileCredentialsDialog: DialogComponent diff --git a/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.tsx b/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.tsx index a0ebb05d804..1e28f0f60ed 100644 --- a/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.tsx +++ b/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.tsx @@ -29,7 +29,7 @@ export interface IAIProfilesTableColumn { type TableColumn = Omit & { render?: IAIProfilesTableColumn['render'] }; -interface Props { +export interface IAIProfilesTableProps { profiles: AIProfile[]; nameLabel: string; engineLabel: string; @@ -48,7 +48,7 @@ const NAME_COLUMN = { key: 'name', minWidth: 120 }; const ENGINE_COLUMN = { key: 'engine', width: 160 }; const SCOPE_COLUMN = { key: 'scope', width: 120 }; -export const AIProfilesTable = observer(function AIProfilesTable({ +export const AIProfilesTable = observer(function AIProfilesTable({ profiles, nameLabel, engineLabel, diff --git a/webapp/packages/plugin-ai-profiles/src/AIProfilesTableLazy.ts b/webapp/packages/plugin-ai-profiles/src/AIProfilesTableLazy.ts new file mode 100644 index 00000000000..5621020a52b --- /dev/null +++ b/webapp/packages/plugin-ai-profiles/src/AIProfilesTableLazy.ts @@ -0,0 +1,11 @@ +/* + * 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 AIProfilesTable = importLazyComponent(() => import('./AIProfilesTable.js').then(module => module.AIProfilesTable)); diff --git a/webapp/packages/plugin-ai-profiles/src/index.ts b/webapp/packages/plugin-ai-profiles/src/index.ts index e31d95b0482..00eee5386f8 100644 --- a/webapp/packages/plugin-ai-profiles/src/index.ts +++ b/webapp/packages/plugin-ai-profiles/src/index.ts @@ -11,5 +11,6 @@ import './module.js'; export * from './AIProfileCredentialsDialogLazy.js'; export * from './AIProfileCredentialsService.js'; export * from './AIProfilesResource.js'; -export * from './AIProfilesTable.js'; +export { AIProfilesTable } from './AIProfilesTableLazy.js'; +export type { IAIProfilesTableColumn, IAIProfilesTableProps } from './AIProfilesTable.js'; export * from './IAIProfileCredentialsDialogPayload.js'; From 66f34a249ec015ddbd46dccb79d614e3064d0a90 Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Fri, 4 Sep 2026 11:01:00 +0200 Subject: [PATCH 14/31] dbeaver/pro#9532 ui adjustments --- .../packages/plugin-ai-profiles/src/AIProfilesTable.tsx | 8 ++++++-- .../src/components/AIProfileCredentialsPanel.tsx | 5 ++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.tsx b/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.tsx index 1e28f0f60ed..5dad266f0b0 100644 --- a/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.tsx +++ b/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.tsx @@ -72,7 +72,12 @@ export const AIProfilesTable = observer(function AIProfil { ...SCOPE_COLUMN, label: 'plugin_ai_profiles_scope', - render: profile => translate(profile.global ? 'plugin_ai_profiles_scope_global' : 'plugin_ai_profiles_scope_user'), + render: profile => ( +
+ {translate(profile.global ? 'plugin_ai_profiles_scope_global' : 'plugin_ai_profiles_scope_user')} + {profile.global && } +
+ ), }, ...additionalColumns, ]; @@ -101,7 +106,6 @@ export const AIProfilesTable = observer(function AIProfil <> {clickable ? {profile.name} : {profile.name}} {getProfileBadge?.(profile)} - {profile.global && } ); diff --git a/webapp/packages/plugin-ai-user-profile/src/components/AIProfileCredentialsPanel.tsx b/webapp/packages/plugin-ai-user-profile/src/components/AIProfileCredentialsPanel.tsx index 56ddbc67c8d..6e387fc7333 100644 --- a/webapp/packages/plugin-ai-user-profile/src/components/AIProfileCredentialsPanel.tsx +++ b/webapp/packages/plugin-ai-user-profile/src/components/AIProfileCredentialsPanel.tsx @@ -45,7 +45,7 @@ const tabs = new TabsContainer('AI Profile Cre tabs.add({ key: CREDENTIALS_TAB_ID, - name: 'plugin_ai_credentials_dialog_title', + name: 'plugin_ai_credentials_profile', panel: () => AIProfileCredentialsFields, }); @@ -88,8 +88,7 @@ const AIProfileCredentialsFields: TabContainerPanelComponent - {translate('plugin_ai_credentials_dialog_description')} + {translate('plugin_ai_credentials_profile')} From 25ca4e302961f2a65204f25baaf7e8af8cf4ec82 Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Fri, 4 Sep 2026 11:25:45 +0200 Subject: [PATCH 15/31] dbeaver/pro#9532 fixes credentials sync --- .../src/AIProfileCredentialsDialog.tsx | 17 +++++++-------- .../src/AIProfileCredentialsService.ts | 1 - .../src/IAIProfileCredentialsDialogPayload.ts | 1 - .../AIProfileCredentialsFormPart.ts | 21 ++++++++++++++++--- .../components/AIProfileCredentialsPanel.tsx | 9 ++++---- 5 files changed, 31 insertions(+), 18 deletions(-) diff --git a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx index 04fb9b668ff..7d5156d8bed 100644 --- a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx +++ b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsDialog.tsx @@ -36,7 +36,6 @@ import { AIProfilesResource } from './AIProfilesResource.js'; interface CredentialsDialogState { token: string; processing: boolean; - credentialsSaved: boolean; } export const AIProfileCredentialsDialog: DialogComponent = observer(function AIProfileCredentialsDialog({ @@ -50,10 +49,11 @@ export const AIProfileCredentialsDialog: DialogComponent({ autofocus: true }); const state = useObservableRef( - () => ({ token: '', processing: false, credentialsSaved: payload.credentialsSaved }), - { token: observable.ref, processing: observable.ref, credentialsSaved: observable.ref }, + () => ({ token: '', processing: false }), + { token: observable.ref, processing: observable.ref }, false, ); + const credentialsSaved = aiProfilesResource.get(payload.profileId)?.credentialsSaved ?? false; const form = useForm({ onSubmit: save }); async function save(): Promise { @@ -73,7 +73,7 @@ export const AIProfileCredentialsDialog: DialogComponent {translate('plugin_ai_credentials_token')}
- {state.credentialsSaved && ( + {credentialsSaved && ( diff --git a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsService.ts b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsService.ts index 74d73a18337..f99cefac8f7 100644 --- a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsService.ts +++ b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentialsService.ts @@ -38,7 +38,6 @@ export class AIProfileCredentialsService { profileName: profile.name, engineName: engine?.name ?? profile.engineId, engineIcon: engine?.icon, - credentialsSaved: profile.credentialsSaved, }); } diff --git a/webapp/packages/plugin-ai-profiles/src/IAIProfileCredentialsDialogPayload.ts b/webapp/packages/plugin-ai-profiles/src/IAIProfileCredentialsDialogPayload.ts index 4fd6ac0a75f..281dbcdcf9a 100644 --- a/webapp/packages/plugin-ai-profiles/src/IAIProfileCredentialsDialogPayload.ts +++ b/webapp/packages/plugin-ai-profiles/src/IAIProfileCredentialsDialogPayload.ts @@ -11,5 +11,4 @@ export interface IAIProfileCredentialsDialogPayload { profileName: string; engineName: string; engineIcon?: string; - credentialsSaved: boolean; } diff --git a/webapp/packages/plugin-ai-user-profile/src/AIProfileCredentialsForm/AIProfileCredentialsFormPart.ts b/webapp/packages/plugin-ai-user-profile/src/AIProfileCredentialsForm/AIProfileCredentialsFormPart.ts index faba409a291..6c4cd6beb27 100644 --- a/webapp/packages/plugin-ai-user-profile/src/AIProfileCredentialsForm/AIProfileCredentialsFormPart.ts +++ b/webapp/packages/plugin-ai-user-profile/src/AIProfileCredentialsForm/AIProfileCredentialsFormPart.ts @@ -15,14 +15,12 @@ import type { IAIProfileCredentialsFormState } from './IAIProfileCredentialsForm export interface IAIProfileCredentialsPartState { profileName: string; engineName: string; - credentialsSaved: boolean; token: string; } const getDefaultState = (): IAIProfileCredentialsPartState => ({ profileName: '', engineName: '', - credentialsSaved: false, token: '', }); @@ -35,6 +33,24 @@ export class AIProfileCredentialsFormPart extends FormPart { try { this.isSaving = true; @@ -60,7 +76,6 @@ export class AIProfileCredentialsFormPart extends FormPart({ autofocus: true }); const part = getAIProfileCredentialsFormPart(formState); const state = part.state; + const credentialsSaved = part.credentialsSaved; useAutoLoad(AIProfileCredentialsFields, part); @@ -102,15 +103,15 @@ const AIProfileCredentialsFields: TabContainerPanelComponent {translate('plugin_ai_credentials_token')} - {state.credentialsSaved && ( + {credentialsSaved && (
+
+ )} + + ); +}); diff --git a/webapp/packages/plugin-ai-user-profile/src/components/AIProfileCredentialsPanel.tsx b/webapp/packages/plugin-ai-user-profile/src/components/AIProfileCredentialsPanel.tsx index 8cf282beb10..43a72d67d18 100644 --- a/webapp/packages/plugin-ai-user-profile/src/components/AIProfileCredentialsPanel.tsx +++ b/webapp/packages/plugin-ai-user-profile/src/components/AIProfileCredentialsPanel.tsx @@ -11,118 +11,24 @@ import { observer } from 'mobx-react-lite'; import { Button, ColoredContainer, - ConfirmationDialog, Container, Form, - Group, GroupBack, GroupTitle, - InputField, - SAVED_VALUE_INDICATOR, StatusMessage, Text, - useAutoLoad, - useFocus, useForm, useTranslate, } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { ENotificationType, NotificationService } from '@cloudbeaver/core-events'; -import { type IFormState, TabList, type TabContainerPanelComponent, TabPanelList, TabsContainer, TabsState } from '@cloudbeaver/core-ui'; +import { TabList, TabPanelList, TabsState } from '@cloudbeaver/core-ui'; import { getFirstException } from '@cloudbeaver/core-utils'; import { getAIProfileCredentialsFormPart } from '../AIProfileCredentialsForm/getAIProfileCredentialsFormPart.js'; -import type { IAIProfileCredentialsFormState } from '../AIProfileCredentialsForm/IAIProfileCredentialsFormState.js'; import { AIProfileCredentialsPanelService } from '../AIProfileCredentialsPanelService.js'; -import { AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; const CREDENTIALS_TAB_ID = 'credentials'; -interface IAIProfileCredentialsFieldsProps { - formState: IFormState; -} - -const tabs = new TabsContainer('AI Profile Credentials'); - -tabs.add({ - key: CREDENTIALS_TAB_ID, - name: 'plugin_ai_credentials_profile', - panel: () => AIProfileCredentialsFields, -}); - -const AIProfileCredentialsFields: TabContainerPanelComponent = observer(function AIProfileCredentialsFields({ - formState, -}) { - const translate = useTranslate(); - const notificationService = useService(NotificationService); - const commonDialogService = useService(CommonDialogService); - const [tokenRef] = useFocus({ autofocus: true }); - const part = getAIProfileCredentialsFormPart(formState); - const aiProfilesResource = useService(AIProfilesResource); - const state = part.state; - const credentialsSaved = part.credentialsSaved; - - useAutoLoad(AIProfileCredentialsFields, part); - - async function resetCredentials(): Promise { - if (!state) { - return; - } - - const { status } = await commonDialogService.open(ConfirmationDialog, { - title: translate('plugin_ai_credentials_reset_title'), - message: 'plugin_ai_credentials_reset_confirmation', - confirmActionText: 'plugin_ai_credentials_reset', - }); - - if (status !== DialogueStateResult.Resolved) { - return; - } - - try { - await aiProfilesResource.resetCredentials(formState.state.profileId); - notificationService.logSuccess({ - title: 'plugin_ai_user_profile_credentials_reset', - message: state.profileName, - }); - } catch (exception: any) { - notificationService.logException(exception, 'plugin_ai_credentials_reset_failed'); - } - } - - return ( - - - - {translate('plugin_ai_credentials_profile')} - - - {translate('plugin_ai_credentials_engine')} - - - {translate('plugin_ai_credentials_token')} - - - {credentialsSaved && ( -
- -
- )} -
- ); -}); export const AIProfileCredentialsPanel = observer(function AIProfileCredentialsPanel() { const translate = useTranslate(); @@ -168,7 +74,7 @@ export const AIProfileCredentialsPanel = observer(function AIProfileCredentialsP
- + { serviceCollection .addSingleton(AIProfileCredentialsPanelService) + .addSingleton(Bootstrap, AIProfileCredentialsFormTabBootstrap) .addSingleton(Bootstrap, LocaleService) .addSingleton(Bootstrap, AIUserProfileBootstrap); }, From 4dbc94c45c114e8e84b8c83f09a8dff9b2cdc5a7 Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Fri, 4 Sep 2026 14:32:18 +0200 Subject: [PATCH 20/31] dbeaver/pro#9532 cleanup 3 --- .../src/AISettingsService.ts | 11 +--- .../AIChatConversationProfile.tsx | 15 +++-- .../AIProfiles/AIEnginePropertiesResource.ts | 7 ++- .../Options/AIProfileFormPart.ts | 15 ++--- .../Options/AIProfileOptions.tsx | 9 ++- .../Options/getAIProfileFormPart.ts | 3 - .../AIProfilesAdministrationService.ts | 56 ------------------ .../src/AIProfiles/AIProfilesPanel.tsx | 7 +-- .../src/AIProfiles/useAIProfilesTable.ts | 8 +-- .../src/module.ts | 2 - .../AIProfileCredentialsDialog.tsx | 16 +++-- .../src/AIProfilesResource.ts | 58 +++++++++---------- .../src/AIUserProfileBootstrap.ts | 2 +- 13 files changed, 62 insertions(+), 147 deletions(-) delete mode 100644 webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesAdministrationService.ts diff --git a/webapp/packages/plugin-ai-administration/src/AISettingsService.ts b/webapp/packages/plugin-ai-administration/src/AISettingsService.ts index e66cbebaf0d..4592d2be2f7 100644 --- a/webapp/packages/plugin-ai-administration/src/AISettingsService.ts +++ b/webapp/packages/plugin-ai-administration/src/AISettingsService.ts @@ -7,20 +7,17 @@ */ import { injectable, IServiceProvider } from '@cloudbeaver/core-di'; import { FormMode } from '@cloudbeaver/core-ui'; -import { AISettingsResource } from '@cloudbeaver/plugin-ai'; import { AdministrationAISettingsFormService } from './AISettingsForm/AdministrationAISettingsFormService.js'; import { AdministrationAISettingsFormState } from './AISettingsForm/AdministrationAISettingsFormState.js'; -import { getAdministrationAISettingsFormInfoPart } from './AISettingsForm/getAdministrationAISettingsFormInfoPart.js'; -@injectable(() => [AdministrationAISettingsFormService, IServiceProvider, AISettingsResource]) +@injectable(() => [AdministrationAISettingsFormService, IServiceProvider]) export class AISettingsService { formState: AdministrationAISettingsFormState | null; constructor( private readonly administrationAISettingsFormService: AdministrationAISettingsFormService, private readonly serviceProvider: IServiceProvider, - private readonly aiSettingsResource: AISettingsResource, ) { this.formState = null; } @@ -35,10 +32,4 @@ export class AISettingsService { this.formState?.dispose(); this.formState = null; } - - isEffectiveDefaultProfile(profileId: string): boolean { - const persistedProfileId = this.aiSettingsResource.data?.defaultConfiguration; - const selectedProfileId = this.formState ? getAdministrationAISettingsFormInfoPart(this.formState).state.defaultConfiguration : null; - return profileId === persistedProfileId || profileId === selectedProfileId; - } } diff --git a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationScope/AIChatConversationProfile.tsx b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationScope/AIChatConversationProfile.tsx index 8af77101543..5925bb21b51 100644 --- a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationScope/AIChatConversationProfile.tsx +++ b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationScope/AIChatConversationProfile.tsx @@ -29,16 +29,15 @@ export const AIChatConversationProfile = observer(function AIChatConversa const translate = useTranslate(); const notificationService = useService(NotificationService); const aiChatConversationsService = useService(AIChatConversationsService); - const credentialsService = useService(AIProfileCredentialsService); + const aiProfileCredentialsService = useService(AIProfileCredentialsService); const menu = useMenuContext(); - const aiEnginesResource = useResource(AIChatConversationProfile, AiEnginesResource, undefined); async function selectProfile(profile: AIProfile) { try { - if (credentialsService.isRequired(profile)) { + if (aiProfileCredentialsService.isRequired(profile)) { menu?.hide(); - const { status } = await credentialsService.open(profile.id); + const { status } = await aiProfileCredentialsService.open(profile.id); if (status !== DialogueStateResult.Resolved) { return; } @@ -49,11 +48,11 @@ export const AIChatConversationProfile = observer(function AIChatConversa } } - async function editCredentials(event: React.MouseEvent, profileId: string): Promise { - event.stopPropagation(); + async function editCredentials(profileId: string): Promise { menu?.hide(); + try { - await credentialsService.open(profileId); + await aiProfileCredentialsService.open(profileId); } catch (exception: any) { notificationService.logException(exception, 'plugin_ai_chat_profile_credentials_edit_fail'); } @@ -101,7 +100,7 @@ export const AIChatConversationProfile = observer(function AIChatConversa disabled={disabled} name="edit" viewBox="0 0 13 13" - onClick={event => editCredentials(event, profile.id)} + onClick={() => editCredentials(profile.id)} /> )}
diff --git a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIEnginePropertiesResource.ts b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIEnginePropertiesResource.ts index 63814f29816..64a431a33b5 100644 --- a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIEnginePropertiesResource.ts +++ b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIEnginePropertiesResource.ts @@ -11,7 +11,7 @@ import { runInAction } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; import { CachedMapResource, type ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-resource'; import { EAdminPermission, SessionPermissionsResource } from '@cloudbeaver/core-root'; -import { GraphQLService, type AiEngineConfig, type IObjectPropertyInfo } from '@cloudbeaver/core-sdk'; +import { GraphQLService, type AiEngineConfig, type AiModelInfo, type IObjectPropertyInfo } from '@cloudbeaver/core-sdk'; export const MODEL_PROPERTY_ID = 'model'; export const CONTEXT_WINDOW_SIZE_PROPERTY_ID = 'contextWindowSize'; @@ -33,6 +33,11 @@ export class AIEnginePropertiesResource extends CachedMapResource { + const { models } = await this.graphQLService.sdk.getEngineModels({ engineId, profileId, settings }); + return models; + } + protected async loader(originalKey: ResourceKeySimple): Promise> { await ResourceKeyUtils.forEachAsync(originalKey, async engineId => { const { properties } = await this.graphQLService.sdk.getEngineProperties({ engineId }); diff --git a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfileFormPart.ts b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfileFormPart.ts index a25628723c7..cd41679f4b3 100644 --- a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfileFormPart.ts +++ b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfileFormPart.ts @@ -9,11 +9,10 @@ import { runInAction } from 'mobx'; import { FormMode, FormPart, formValidationContext, type IFormState } from '@cloudbeaver/core-ui'; import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; -import type { AiEngineConfig } from '@cloudbeaver/core-sdk'; +import type { AiConfigurationProfileInput, AiEngineConfig } from '@cloudbeaver/core-sdk'; import { getUniqueName } from '@cloudbeaver/core-utils'; import { AIEnginePropertiesResource } from '../../AIEnginePropertiesResource.js'; -import { AIProfilesAdministrationService, type AIAdminProfile, type AIProfileInput } from '../../AIProfilesAdministrationService.js'; import { getObjectPropertiesValues } from '../../utils/getObjectPropertiesValues.js'; import { prepareProperties } from '../../utils/prepareProperties.js'; import type { IAIProfileFormState } from '../IAIProfileFormState.js'; @@ -33,7 +32,6 @@ export class AIProfileFormPart extends FormPart, private readonly aiProfilesResource: AIProfilesResource, - private readonly aiProfilesAdministrationService: AIProfilesAdministrationService, private readonly aiProfileCredentialsService: AIProfileCredentialsService, private readonly aiEnginePropertiesResource: AIEnginePropertiesResource, ) { @@ -118,7 +116,7 @@ export class AIProfileFormPart extends FormPart { const config = this.getConfig(); + const creating = this.formState.mode === FormMode.Create; + const profile = creating ? await this.aiProfilesResource.createProfile(config) : await this.aiProfilesResource.updateProfile(config); - let profile: AIAdminProfile; - - if (this.formState.mode === FormMode.Create) { - profile = await this.aiProfilesAdministrationService.create(config); + if (creating) { this.formState.setMode(FormMode.Edit); - } else { - profile = await this.aiProfilesAdministrationService.update(config); } this.formState.state.name = profile.name; diff --git a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfileOptions.tsx b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfileOptions.tsx index ce8a7fd6f1e..7b76ce81d70 100644 --- a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfileOptions.tsx +++ b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfileOptions.tsx @@ -39,7 +39,6 @@ import { MODEL_PROPERTY_ID, TEMPERATURE_PROPERTY_ID, } from '../../AIEnginePropertiesResource.js'; -import { AIProfilesAdministrationService } from '../../AIProfilesAdministrationService.js'; import type { IAIProfileFormProps } from '../IAIProfileFormProps.js'; import { AIProfilePropertiesForm } from './AIProfilePropertiesForm.js'; import { AI_PROFILE_NAME_MAX_LENGTH, AI_PROFILE_NAME_MIN_LENGTH } from './AIProfileSchema.js'; @@ -49,7 +48,7 @@ export const AIProfileOptions: TabContainerPanelComponent = const translate = useTranslate(); const notificationService = useService(NotificationService); const aiProfileCredentialsService = useService(AIProfileCredentialsService); - const aiProfilesAdministrationService = useService(AIProfilesAdministrationService); + const aiEnginePropertiesResource = useService(AIEnginePropertiesResource); const enginesLoader = useResource(AIProfileOptions, AiEnginesResource, undefined); const part = getAIProfileFormPart(formState); const propertiesLoader = useResource(AIProfileOptions, AIEnginePropertiesResource, part.state.engineId || null); @@ -105,9 +104,9 @@ export const AIProfileOptions: TabContainerPanelComponent = try { setIsLoading(true); const profileId = formState.mode === FormMode.Edit ? formState.state.profileId : undefined; - const loadedModels = (await aiProfilesAdministrationService.loadModels(engineId, profileId, part.getCurrentEngineSettings())).toSorted((a, b) => - a.id.localeCompare(b.id), - ); + const loadedModels = ( + await aiEnginePropertiesResource.loadModels(engineId, profileId, part.getCurrentEngineSettings()) + ).toSorted((a, b) => a.id.localeCompare(b.id)); setModels(loadedModels); return loadedModels; } catch (error: any) { diff --git a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/getAIProfileFormPart.ts b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/getAIProfileFormPart.ts index 82002fc656c..90be6d604a6 100644 --- a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/getAIProfileFormPart.ts +++ b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/getAIProfileFormPart.ts @@ -10,7 +10,6 @@ import type { IFormState } from '@cloudbeaver/core-ui'; import { AIProfileCredentialsService, AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; import { AIEnginePropertiesResource } from '../../AIEnginePropertiesResource.js'; -import { AIProfilesAdministrationService } from '../../AIProfilesAdministrationService.js'; import type { IAIProfileFormState } from '../IAIProfileFormState.js'; import { AIProfileFormPart } from './AIProfileFormPart.js'; @@ -20,14 +19,12 @@ export function getAIProfileFormPart(formState: IFormState) return formState.getPart(DATA_CONTEXT_AI_PROFILE_FORM_PART, context => { const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; const aiProfilesResource = di.getService(AIProfilesResource); - const aiProfilesAdministrationService = di.getService(AIProfilesAdministrationService); const aiProfileCredentialsService = di.getService(AIProfileCredentialsService); const aiEnginePropertiesResource = di.getService(AIEnginePropertiesResource); return new AIProfileFormPart( formState, aiProfilesResource, - aiProfilesAdministrationService, aiProfileCredentialsService, aiEnginePropertiesResource, ); diff --git a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesAdministrationService.ts b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesAdministrationService.ts deleted file mode 100644 index 4480ac45b07..00000000000 --- a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesAdministrationService.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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 { injectable } from '@cloudbeaver/core-di'; -import { - GraphQLService, - type AiAdminConfigurationProfileInfo, - type AiConfigurationProfileInput, - type AiEngineConfig, - type AiModelInfo, -} from '@cloudbeaver/core-sdk'; -import { AISettingsResource } from '@cloudbeaver/plugin-ai'; -import { AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; - -export type AIAdminProfile = AiAdminConfigurationProfileInfo; -export type AIProfileInput = AiConfigurationProfileInput; - -// TODO do we need this service? -@injectable(() => [GraphQLService, AIProfilesResource, AISettingsResource]) -export class AIProfilesAdministrationService { - constructor( - private readonly graphQLService: GraphQLService, - private readonly aiProfilesResource: AIProfilesResource, - private readonly aiSettingsResource: AISettingsResource, - ) {} - - async create(config: AIProfileInput): Promise { - const { profile } = await this.graphQLService.sdk.createAiProfile({ config }); - this.aiProfilesResource.setProfile(profile); - this.aiSettingsResource.markOutdated(); - return profile; - } - - async update(config: AIProfileInput): Promise { - const { profile } = await this.graphQLService.sdk.updateAiProfile({ config }); - this.aiProfilesResource.setProfile(profile); - this.aiSettingsResource.markOutdated(); - return profile; - } - - async delete(profileId: string): Promise { - await this.graphQLService.sdk.deleteAiProfile({ profileId }); - this.aiProfilesResource.removeProfile(profileId); - this.aiSettingsResource.markOutdated(); - } - - async loadModels(engineId: string, profileId?: string, settings?: AiEngineConfig): Promise { - const { models } = await this.graphQLService.sdk.getEngineModels({ engineId, profileId, settings }); - return models; - } -} diff --git a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesPanel.tsx b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesPanel.tsx index 301da517bfb..49828c0bfc9 100644 --- a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesPanel.tsx +++ b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfilesPanel.tsx @@ -24,7 +24,6 @@ import { import { useService } from '@cloudbeaver/core-di'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; import { AISettingsResource } from '@cloudbeaver/plugin-ai'; -import { AISettingsService } from '@cloudbeaver/plugin-ai-administration'; import { AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; import { TableSelectionContext, useTableSelection } from '@cloudbeaver/plugin-data-grid'; import { isDefined } from '@dbeaver/js-helpers'; @@ -42,16 +41,16 @@ const toolsPanelRegistry: StyleRegistry = [ export const AIProfilesPanel = observer(function AIProfilesPanel() { const translate = useTranslate(); const aiProfileFormService = useService(AIProfileFormService); - const aiSettingsService = useService(AISettingsService); const aiSettingsResource = useService(AISettingsResource); useResource(AIProfilesPanel, AISettingsResource, undefined); const profilesLoader = useResource(AIProfilesPanel, AIProfilesResource, CachedMapAllKey); const profiles = profilesLoader.data.filter(isDefined); const settingsLoaded = aiSettingsResource.isLoaded(); + const defaultProfileId = aiSettingsResource.data?.defaultConfiguration; const selection = useTableSelection( - profiles.filter(profile => settingsLoaded && !aiSettingsService.isEffectiveDefaultProfile(profile.id)).map(profile => profile.id), + profiles.filter(profile => settingsLoaded && profile.id !== defaultProfileId).map(profile => profile.id), ); const table = useAIProfilesTable(selection); @@ -95,7 +94,7 @@ export const AIProfilesPanel = observer(function AIProfilesPanel() { aiSettingsService.isEffectiveDefaultProfile(profileId)} + isDefaultProfile={profileId => profileId === defaultProfileId} />
diff --git a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/useAIProfilesTable.ts b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/useAIProfilesTable.ts index 610b28483f3..87c61df26d8 100644 --- a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/useAIProfilesTable.ts +++ b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/useAIProfilesTable.ts @@ -16,12 +16,9 @@ import { CachedMapAllKey } from '@cloudbeaver/core-resource'; import { AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; import type { ITableSelection } from '@cloudbeaver/plugin-data-grid'; -import { AIProfilesAdministrationService } from './AIProfilesAdministrationService.js'; - interface State { processing: boolean; aiProfilesResource: AIProfilesResource; - aiProfilesAdministrationService: AIProfilesAdministrationService; notificationService: NotificationService; dialogService: CommonDialogService; selection: ITableSelection; @@ -33,7 +30,6 @@ export function useAIProfilesTable(selection: ITableSelection): Readonly const notificationService = useService(NotificationService); const dialogService = useService(CommonDialogService); const aiProfilesResource = useService(AIProfilesResource); - const aiProfilesAdministrationService = useService(AIProfilesAdministrationService); const translate = useTranslate(); return useObservableRef( @@ -84,7 +80,7 @@ export function useAIProfilesTable(selection: ITableSelection): Readonly try { this.processing = true; - const results = await Promise.allSettled(deletionList.map(profileId => this.aiProfilesAdministrationService.delete(profileId))); + const results = await Promise.allSettled(deletionList.map(profileId => this.aiProfilesResource.deleteProfile(profileId))); const failed = results.filter((r): r is PromiseRejectedResult => r.status === 'rejected'); if (failed.length === 0) { @@ -108,6 +104,6 @@ export function useAIProfilesTable(selection: ITableSelection): Readonly refresh: action.bound, delete: action.bound, }, - { aiProfilesResource, aiProfilesAdministrationService, selection, notificationService, dialogService }, + { aiProfilesResource, selection, notificationService, dialogService }, ); } diff --git a/webapp/packages/plugin-ai-profiles-administration/src/module.ts b/webapp/packages/plugin-ai-profiles-administration/src/module.ts index 5d0c4071ba0..36d4fedc293 100644 --- a/webapp/packages/plugin-ai-profiles-administration/src/module.ts +++ b/webapp/packages/plugin-ai-profiles-administration/src/module.ts @@ -11,7 +11,6 @@ import { Bootstrap, Dependency, ModuleRegistry, proxy } from '@cloudbeaver/core- import { AIEnginePropertiesResource } from './AIProfiles/AIEnginePropertiesResource.js'; import { AIProfileFormService } from './AIProfiles/AIProfileForm/AIProfileFormService.js'; import { AIProfileFormTabBootstrap } from './AIProfiles/AIProfileForm/AIProfileFormTabBootstrap.js'; -import { AIProfilesAdministrationService } from './AIProfiles/AIProfilesAdministrationService.js'; import { AIProfilesAdministrationBootstrap } from './AIProfilesAdministrationBootstrap.js'; import { LocaleService } from './LocaleService.js'; @@ -25,7 +24,6 @@ export default ModuleRegistry.add({ .addSingleton(Bootstrap, LocaleService) .addSingleton(Dependency, proxy(AIEnginePropertiesResource)) .addSingleton(AIProfilesAdministrationBootstrap) - .addSingleton(AIProfilesAdministrationService) .addSingleton(AIEnginePropertiesResource) .addSingleton(AIProfileFormService); }, diff --git a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentials/AIProfileCredentialsDialog.tsx b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentials/AIProfileCredentialsDialog.tsx index 5f99b3a7c9d..4d67fa4ae18 100644 --- a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentials/AIProfileCredentialsDialog.tsx +++ b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentials/AIProfileCredentialsDialog.tsx @@ -64,16 +64,16 @@ export const AIProfileCredentialsDialog: DialogComponent { userInfoResource.onUserChange.addHandler(() => this.markOutdated(CachedMapAllKey)); } - setProfile(profile: Omit & Partial>): void { - this.set(profile.id, { - ...profile, - credentialsSaved: profile.credentialsSaved ?? this.get(profile.id)?.credentialsSaved ?? false, - }); + async createProfile(config: AiConfigurationProfileInput): Promise { + const { profile } = await this.graphQLService.sdk.createAiProfile({ config }); + this.set(profile.id, { ...profile, credentialsSaved: false }); + return profile; } - removeProfile(profileId: string): void { - this.delete(profileId); + async updateProfile(config: AiConfigurationProfileInput): Promise { + const { profile } = await this.graphQLService.sdk.updateAiProfile({ config }); + this.set(profile.id, { ...profile, credentialsSaved: this.get(profile.id)?.credentialsSaved ?? false }); + return profile; } - setCredentialsSaved(profileId: string, credentialsSaved: boolean): void { - const profile = this.get(profileId); - if (profile) { - this.set(profileId, { ...profile, credentialsSaved }); - } + async deleteProfile(profileId: string): Promise { + await this.graphQLService.sdk.deleteAiProfile({ profileId }); + this.delete(profileId); } - saveCredentials(profileId: string, token: string): Promise { - if (!token) { - return Promise.resolve(false); - } - - const profile = this.get(profileId); - if (!profile || profile.global) { - return Promise.resolve(false); - } + saveCredentials(profileId: string, token: string): Promise { return this.updateCredentials(profileId, token, true); } - resetCredentials(profileId: string): Promise { - const profile = this.get(profileId); - if (!profile || profile.global) { - return Promise.resolve(false); - } + resetCredentials(profileId: string): Promise { return this.updateCredentials(profileId, '', false); } @@ -82,14 +74,16 @@ export class AIProfilesResource extends CachedMapResource { return typeof key === 'string'; } - private async updateCredentials(profileId: string, token: string, credentialsSaved: boolean): Promise { - const { result } = await this.graphQLService.sdk.saveAiProfileCredentials({ + private async updateCredentials(profileId: string, token: string, credentialsSaved: boolean): Promise { + await this.graphQLService.sdk.saveAiProfileCredentials({ profileId, credentials: { properties: { token } }, }); - if (result) { - this.setCredentialsSaved(profileId, credentialsSaved); + + const profile = this.get(profileId); + if (profile) { + this.set(profileId, { ...profile, credentialsSaved }); } - return result; + this.markOutdated(profileId); } } diff --git a/webapp/packages/plugin-ai-user-profile/src/AIUserProfileBootstrap.ts b/webapp/packages/plugin-ai-user-profile/src/AIUserProfileBootstrap.ts index 49611c58bdb..ca068f9a52e 100644 --- a/webapp/packages/plugin-ai-user-profile/src/AIUserProfileBootstrap.ts +++ b/webapp/packages/plugin-ai-user-profile/src/AIUserProfileBootstrap.ts @@ -46,7 +46,7 @@ export class AIUserProfileBootstrap extends Bootstrap { return ( this.appAuthService.authenticated && this.serverConfigResource.isFeatureEnabled(FEATURE_AI_ID, true) && - (!this.aiProfilesResource.isLoaded(CachedMapAllKey) || this.aiProfilesResource.values.length > 0) + this.aiProfilesResource.values.length > 0 ); } } From 834e13f02323ba3cd97defc72bb4a8c7ab699e89 Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Fri, 4 Sep 2026 14:45:46 +0200 Subject: [PATCH 21/31] dbeaver/pro#9532 closes modal dialog after successful token saving --- .../AIProfileCredentialsDialog.tsx | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentials/AIProfileCredentialsDialog.tsx b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentials/AIProfileCredentialsDialog.tsx index 4d67fa4ae18..1b6199767e4 100644 --- a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentials/AIProfileCredentialsDialog.tsx +++ b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentials/AIProfileCredentialsDialog.tsx @@ -63,20 +63,13 @@ export const AIProfileCredentialsDialog: DialogComponent Date: Fri, 4 Sep 2026 15:04:48 +0200 Subject: [PATCH 22/31] dbeaver/pro#9532 cleanup 4 --- .../plugin-ai-user-profile/src/AIUserProfileBootstrap.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/webapp/packages/plugin-ai-user-profile/src/AIUserProfileBootstrap.ts b/webapp/packages/plugin-ai-user-profile/src/AIUserProfileBootstrap.ts index ca068f9a52e..fc8d16f8ca2 100644 --- a/webapp/packages/plugin-ai-user-profile/src/AIUserProfileBootstrap.ts +++ b/webapp/packages/plugin-ai-user-profile/src/AIUserProfileBootstrap.ts @@ -33,10 +33,7 @@ export class AIUserProfileBootstrap extends Bootstrap { key: AI_PROFILES_TAB_ID, name: 'plugin_ai_user_profile_tab_label', order: 4, - getLoader: () => - getCachedMapResourceLoaderState(this.aiProfilesResource, () => - this.appAuthService.authenticated && this.serverConfigResource.isFeatureEnabled(FEATURE_AI_ID, true) ? CachedMapAllKey : null, - ), + getLoader: () => getCachedMapResourceLoaderState(this.aiProfilesResource, () => CachedMapAllKey), isHidden: () => !this.isAvailable(), panel: () => AIProfilesPanel, }); From 816af1073661cea263177ff555dcde43b80267f9 Mon Sep 17 00:00:00 2001 From: Ainur Date: Mon, 7 Sep 2026 11:06:55 +0200 Subject: [PATCH 23/31] dbeaver/pro#9532 use default profile if previous was deleted --- .../io/cloudbeaver/service/ai/WebAIProfileCredentials.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIProfileCredentials.java b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIProfileCredentials.java index 16d597ca830..9a782e5f302 100644 --- a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIProfileCredentials.java +++ b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIProfileCredentials.java @@ -32,7 +32,10 @@ import org.jkiss.dbeaver.runtime.properties.PropertySourceEditable; import org.jkiss.utils.CommonUtils; -import java.util.*; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; public final class WebAIProfileCredentials { private static final String SECRET_ID_PREFIX = "ai.profile."; @@ -93,7 +96,7 @@ public static AIConfigurationProfile getEffectiveProfile( AIConfigurationProfile source = AISettingsManager.getStaticSettings() .getConfigurationOrNull(profile.getProfileId()); if (source == null) { - throw new DBWebException("AI profile does not exist"); + source = AISettingsManager.getStaticSettings().getDefaultConfiguration(); } if (source.isGlobal()) { return source; From 3700a5238341e6eed914909032a14ef0159c4947 Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Tue, 8 Sep 2026 16:54:46 +0200 Subject: [PATCH 24/31] dbeaver/pro#9532 fixes sync for profiles --- .../AIChatConversation/AIChatConversationsResource.ts | 5 ++--- .../packages/plugin-ai-profiles/src/AIProfilesResource.ts | 8 +++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationsResource.ts b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationsResource.ts index c4538c4494c..2df6442a31e 100644 --- a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationsResource.ts +++ b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationsResource.ts @@ -48,9 +48,8 @@ export class AIChatConversationsResource extends CachedMapResource { const deletedProfileIds = ResourceKeyUtils.toArray(key); - const conversationIds = this.values - .filter(conversation => conversation.profile && deletedProfileIds.includes(conversation.profile)) - .map(conversation => conversation.id); + const conversations = this.values.filter(conversation => conversation.profile && deletedProfileIds.includes(conversation.profile)); + const conversationIds = conversations.map(conversation => conversation.id); this.markOutdated(resourceKeyList(conversationIds)); }); diff --git a/webapp/packages/plugin-ai-profiles/src/AIProfilesResource.ts b/webapp/packages/plugin-ai-profiles/src/AIProfilesResource.ts index 14165f85c99..6c27c1962fd 100644 --- a/webapp/packages/plugin-ai-profiles/src/AIProfilesResource.ts +++ b/webapp/packages/plugin-ai-profiles/src/AIProfilesResource.ts @@ -16,16 +16,18 @@ import { type AiConfigurationProfileInput, GraphQLService, } from '@cloudbeaver/core-sdk'; +import { AISettingsResource } from '@cloudbeaver/plugin-ai'; export type AIProfile = AiConfigurationProfileInfo; -@injectable(() => [GraphQLService, ServerConfigResource, WorkspaceConfigEventHandler, UserInfoResource]) +@injectable(() => [GraphQLService, ServerConfigResource, WorkspaceConfigEventHandler, UserInfoResource, AISettingsResource]) export class AIProfilesResource extends CachedMapResource { constructor( private readonly graphQLService: GraphQLService, serverConfigResource: ServerConfigResource, workspaceConfigEventHandler: WorkspaceConfigEventHandler, userInfoResource: UserInfoResource, + private readonly aiSettingsResource: AISettingsResource, ) { super(); @@ -40,8 +42,12 @@ export class AIProfilesResource extends CachedMapResource { } async createProfile(config: AiConfigurationProfileInput): Promise { + const firstProfile = this.values.length === 0; const { profile } = await this.graphQLService.sdk.createAiProfile({ config }); this.set(profile.id, { ...profile, credentialsSaved: false }); + if (firstProfile) { + this.aiSettingsResource.markOutdated(); + } return profile; } From 9ff560f64f89e633b87d9fd85a5bd9d3b868de09 Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Thu, 10 Sep 2026 15:20:08 +0200 Subject: [PATCH 25/31] dbeaver/pro#9532 pr review fixes --- .../AIChatConversationScope.tsx | 4 +- .../AIChatMessage/AIChatMessageService.ts | 23 +------- .../AIChatProfileCredentialsBootstrap.ts | 44 ++++++++++++++ webapp/packages/plugin-ai-chat/src/module.ts | 2 + .../Options/AIProfileOptions.tsx | 24 ++++---- .../src/AIProfiles/useAIProfilesTable.ts | 4 +- .../src/AIProfilesAdministrationBootstrap.ts | 4 +- .../src/AIProfilesTabPanel.tsx | 14 ----- .../packages/plugin-ai-profiles/package.json | 4 +- .../AIProfileCredentialsDialog.tsx | 59 +++++++++---------- .../AIProfileCredentialsService.ts | 11 ++-- .../AIProfileCredentialsUtils.ts | 17 ------ .../IAIProfileCredentialsDialogPayload.ts | 14 ----- .../src/AIProfilesTable.module.css | 23 -------- .../src/AIProfilesTable.tsx | 4 +- .../packages/plugin-ai-profiles/src/index.ts | 2 +- .../packages/plugin-ai-profiles/tsconfig.json | 3 + webapp/yarn.lock | 2 +- 18 files changed, 111 insertions(+), 147 deletions(-) create mode 100644 webapp/packages/plugin-ai-chat/src/AIChat/AIChatProfileCredentialsBootstrap.ts delete mode 100644 webapp/packages/plugin-ai-profiles-administration/src/AIProfilesTabPanel.tsx delete mode 100644 webapp/packages/plugin-ai-profiles/src/AIProfileCredentials/AIProfileCredentialsUtils.ts delete mode 100644 webapp/packages/plugin-ai-profiles/src/AIProfileCredentials/IAIProfileCredentialsDialogPayload.ts delete mode 100644 webapp/packages/plugin-ai-profiles/src/AIProfilesTable.module.css diff --git a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationScope/AIChatConversationScope.tsx b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationScope/AIChatConversationScope.tsx index 7fecb1e281b..5e4cfeef13b 100644 --- a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationScope/AIChatConversationScope.tsx +++ b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatConversation/AIChatConversationScope/AIChatConversationScope.tsx @@ -47,8 +47,8 @@ export const AIChatConversationScope = observer(function AIChatConversati const { data: container } = useResource(AIChatConversationScope, ContainerResource, conversation.dataSourceId ?? null); const { data: currentScope } = useResource(AIChatConversationScope, AIChatConversationScopeResource, conversation.id); - const { data: profileList } = useResource(AIChatConversationScope, AIProfilesResource, CachedMapAllKey); - const profiles = profileList.filter(isNotNullDefined); + const aiProfilesResource = useResource(AIChatConversationScope, AIProfilesResource, CachedMapAllKey); + const profiles = aiProfilesResource.data.filter(isNotNullDefined); async function selectScope(scope: AiDatabaseScope) { if (!conversation.dataSourceId) { diff --git a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/AIChatMessageService.ts b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/AIChatMessageService.ts index 80277eb6d67..5a96bef73df 100644 --- a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/AIChatMessageService.ts +++ b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/AIChatMessageService.ts @@ -15,7 +15,6 @@ import { LocalizationService } from '@cloudbeaver/core-localization'; import { Executor, ExecutorInterrupter } from '@cloudbeaver/core-executor'; import { ConnectionsManagerService, type IConnectionInfoParams } from '@cloudbeaver/core-connections'; import type { AiSendChatMessageInfoFragment } from '@cloudbeaver/core-sdk'; -import { AIProfileCredentialsService, AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; import { AIChatMessagesResource, isFunctionConfirmationMessage, isFunctionMessage, type IMessageParam } from './AIChatMessagesResource.js'; import { AIChatConversationsResource } from '../AIChatConversation/AIChatConversationsResource.js'; @@ -45,15 +44,7 @@ interface IMessageSendExecutorAfterData { type MessageSendExecutorData = IMessageSendExecutorBeforeData | IMessageSendExecutorAfterData; -@injectable(() => [ - AIChatMessagesResource, - AIChatConversationsResource, - CommonDialogService, - LocalizationService, - ConnectionsManagerService, - AIProfilesResource, - AIProfileCredentialsService, -]) +@injectable(() => [AIChatMessagesResource, AIChatConversationsResource, CommonDialogService, LocalizationService, ConnectionsManagerService]) export class AIChatMessageService { onMessageSend: Executor; @@ -63,8 +54,6 @@ export class AIChatMessageService { private readonly commonDialogService: CommonDialogService, private readonly localizationService: LocalizationService, private readonly connectionsManagerService: ConnectionsManagerService, - private readonly aiProfilesResource: AIProfilesResource, - private readonly credentialsService: AIProfileCredentialsService, ) { this.onMessageSend = new Executor(); @@ -125,16 +114,6 @@ export class AIChatMessageService { async processSendMessageAction(conversationId: string, action: () => Promise) { const conversation = await this.aiChatConversationsResource.load(conversationId); - const profileId = conversation.profile; - const profile = profileId ? await this.aiProfilesResource.load(profileId) : undefined; - - if (profile && this.credentialsService.isRequired(profile)) { - const { status } = await this.credentialsService.open(profile.id); - if (status !== DialogueStateResult.Resolved) { - return; - } - } - const contexts = await this.onMessageSend.execute({ stage: 'before', data: { conversationId, connectionKey: conversation.dataSourceId } }); if (ExecutorInterrupter.isInterrupted(contexts)) { diff --git a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatProfileCredentialsBootstrap.ts b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatProfileCredentialsBootstrap.ts new file mode 100644 index 00000000000..c77de443e33 --- /dev/null +++ b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatProfileCredentialsBootstrap.ts @@ -0,0 +1,44 @@ +/* + * 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 { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { ExecutorInterrupter } from '@cloudbeaver/core-executor'; +import { AIProfileCredentialsService, AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; + +import { AIChatConversationsResource } from './AIChatConversation/AIChatConversationsResource.js'; +import { AIChatMessageService } from './AIChatMessage/AIChatMessageService.js'; + +@injectable(() => [AIChatMessageService, AIChatConversationsResource, AIProfilesResource, AIProfileCredentialsService]) +export class AIChatProfileCredentialsBootstrap extends Bootstrap { + constructor( + aiChatMessageService: AIChatMessageService, + aiChatConversationsResource: AIChatConversationsResource, + aiProfilesResource: AIProfilesResource, + credentialsService: AIProfileCredentialsService, + ) { + super(); + + aiChatMessageService.onMessageSend.addHandler(async (event, contexts) => { + if (event.stage !== 'before') { + return; + } + + const conversation = await aiChatConversationsResource.load(event.data.conversationId); + const profile = conversation.profile ? await aiProfilesResource.load(conversation.profile) : undefined; + + if (profile && credentialsService.isRequired(profile)) { + const { status } = await credentialsService.open(profile.id); + + if (status !== DialogueStateResult.Resolved) { + ExecutorInterrupter.interrupt(contexts); + } + } + }); + } +} diff --git a/webapp/packages/plugin-ai-chat/src/module.ts b/webapp/packages/plugin-ai-chat/src/module.ts index 99563334228..d4938ef082c 100644 --- a/webapp/packages/plugin-ai-chat/src/module.ts +++ b/webapp/packages/plugin-ai-chat/src/module.ts @@ -23,6 +23,7 @@ import { AIChatConversationScopeResource } from './AIChat/AIChatConversation/AIC import { AIChatConversationMetricsResource } from './AIChat/AIChatConversation/AIChatConversationMetricsResource.js'; import { AIChatFunctionsService } from './AIChatFunctionsService.js'; import { AIFunctionsResource } from './AIFunctionsResource.js'; +import { AIChatProfileCredentialsBootstrap } from './AIChat/AIChatProfileCredentialsBootstrap.js'; export default ModuleRegistry.add({ name: '@cloudbeaver/plugin-ai-chat', @@ -31,6 +32,7 @@ export default ModuleRegistry.add({ serviceCollection .addSingleton(Bootstrap, LocaleService) .addSingleton(Bootstrap, AIChatServiceBootstrap) + .addSingleton(Bootstrap, AIChatProfileCredentialsBootstrap) .addSingleton(Bootstrap, proxy(AIChatContextService)) .addSingleton(Dependency, proxy(AIChatSettingsService)) .addSingleton(Dependency, proxy(AIChatMessagesResource)) diff --git a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfileOptions.tsx b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfileOptions.tsx index 7b76ce81d70..e4e4714359b 100644 --- a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfileOptions.tsx +++ b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/AIProfileForm/Options/AIProfileOptions.tsx @@ -55,8 +55,16 @@ export const AIProfileOptions: TabContainerPanelComponent = const propertiesInfo = propertiesLoader.data ?? []; const usesUserCredentials = !part.state.global; const configurableProperties = propertiesInfo - .filter(property => property.id !== 'global' && (!usesUserCredentials || property.id !== 'token')) - .map(property => (part.state.global && property.id === 'token' ? { ...property, required: true } : property)); + .filter(({ id }) => { + if (id === 'global') { + return false; + } + if (id === 'token') { + return !usesUserCredentials; + } + return true; + }) + .map(property => (property.id === 'token' ? { ...property, required: true } : property)); const isEditMode = formState.mode === FormMode.Edit; const [isLoading, setIsLoading] = useState(false); const [models, setModels] = useState(null); @@ -104,9 +112,9 @@ export const AIProfileOptions: TabContainerPanelComponent = try { setIsLoading(true); const profileId = formState.mode === FormMode.Edit ? formState.state.profileId : undefined; - const loadedModels = ( - await aiEnginePropertiesResource.loadModels(engineId, profileId, part.getCurrentEngineSettings()) - ).toSorted((a, b) => a.id.localeCompare(b.id)); + const loadedModels = (await aiEnginePropertiesResource.loadModels(engineId, profileId, part.getCurrentEngineSettings())).toSorted((a, b) => + a.id.localeCompare(b.id), + ); setModels(loadedModels); return loadedModels; } catch (error: any) { @@ -245,11 +253,7 @@ export const AIProfileOptions: TabContainerPanelComponent = )} )} - + )} diff --git a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/useAIProfilesTable.ts b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/useAIProfilesTable.ts index 87c61df26d8..d442c09f92c 100644 --- a/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/useAIProfilesTable.ts +++ b/webapp/packages/plugin-ai-profiles-administration/src/AIProfiles/useAIProfilesTable.ts @@ -62,8 +62,8 @@ export function useAIProfilesTable(selection: ITableSelection): Readonly } const names = deletionList.map(id => `"${this.aiProfilesResource.get(id)?.name ?? id}"`).join(', '); - const deletesUserCredentials = deletionList.some(id => this.aiProfilesResource.get(id)?.global === false); - const credentialsWarning = deletesUserCredentials + const shouldDeleteUserCredentials = deletionList.some(id => this.aiProfilesResource.get(id)?.global === false); + const credentialsWarning = shouldDeleteUserCredentials ? `\n\n${translate('plugin_ai_administration_profile_delete_user_credentials_warning')}` : ''; const message = `${translate('plugin_ai_administration_profile_delete_confirmation')}${names}.${credentialsWarning}\n\n${translate('ui_are_you_sure')}`; diff --git a/webapp/packages/plugin-ai-profiles-administration/src/AIProfilesAdministrationBootstrap.ts b/webapp/packages/plugin-ai-profiles-administration/src/AIProfilesAdministrationBootstrap.ts index 80db2e81406..b32b94138e3 100644 --- a/webapp/packages/plugin-ai-profiles-administration/src/AIProfilesAdministrationBootstrap.ts +++ b/webapp/packages/plugin-ai-profiles-administration/src/AIProfilesAdministrationBootstrap.ts @@ -10,7 +10,7 @@ import { importLazyComponent } from '@cloudbeaver/core-blocks'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { AIAdministrationBootstrap, AIAdministrationTabsService, EAIAdministrationSub } from '@cloudbeaver/plugin-ai-administration'; -const AIProfilesTabPanel = importLazyComponent(() => import('./AIProfilesTabPanel.js').then(module => module.AIProfilesTabPanel)); +const AIProfilesPanel = importLazyComponent(() => import('./AIProfiles/AIProfilesPanel.js').then(module => module.AIProfilesPanel)); @injectable(() => [AIAdministrationBootstrap, AIAdministrationTabsService]) export class AIProfilesAdministrationBootstrap extends Bootstrap { @@ -26,7 +26,7 @@ export class AIProfilesAdministrationBootstrap extends Bootstrap { key: EAIAdministrationSub.Profiles, name: 'plugin_ai_administration_profiles_title', order: 2, - panel: () => AIProfilesTabPanel, + panel: () => AIProfilesPanel, }); this.aiAdministrationBootstrap.administrationItem.sub.push({ name: EAIAdministrationSub.Profiles }); } diff --git a/webapp/packages/plugin-ai-profiles-administration/src/AIProfilesTabPanel.tsx b/webapp/packages/plugin-ai-profiles-administration/src/AIProfilesTabPanel.tsx deleted file mode 100644 index 1a184450bd5..00000000000 --- a/webapp/packages/plugin-ai-profiles-administration/src/AIProfilesTabPanel.tsx +++ /dev/null @@ -1,14 +0,0 @@ -/* - * 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 { AIProfilesPanel } from './AIProfiles/AIProfilesPanel.js'; - -export const AIProfilesTabPanel = observer(function AIProfilesTabPanel() { - return ; -}); diff --git a/webapp/packages/plugin-ai-profiles/package.json b/webapp/packages/plugin-ai-profiles/package.json index 5f49201fc5d..a1bd6c4d5cf 100644 --- a/webapp/packages/plugin-ai-profiles/package.json +++ b/webapp/packages/plugin-ai-profiles/package.json @@ -21,6 +21,7 @@ "validate-dependencies": "core-cli-validate-dependencies" }, "dependencies": { + "@cloudbeaver/core-administration": "workspace:*", "@cloudbeaver/core-authentication": "workspace:*", "@cloudbeaver/core-blocks": "workspace:*", "@cloudbeaver/core-di": "workspace:*", @@ -44,7 +45,6 @@ "@cloudbeaver/tsconfig": "workspace:*", "@types/react": "^19", "rimraf": "^6", - "typescript": "^5", - "typescript-plugin-css-modules": "^5" + "typescript": "^5" } } diff --git a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentials/AIProfileCredentialsDialog.tsx b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentials/AIProfileCredentialsDialog.tsx index 1b6199767e4..c30239e0b21 100644 --- a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentials/AIProfileCredentialsDialog.tsx +++ b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentials/AIProfileCredentialsDialog.tsx @@ -6,8 +6,8 @@ * you may not use this file except in compliance with the License. */ -import { observable } from 'mobx'; import { observer } from 'mobx-react-lite'; +import { useState } from 'react'; import { Button, @@ -23,19 +23,20 @@ import { SAVED_VALUE_INDICATOR, useFocus, useForm, - useObservableRef, + useResource, useTranslate, } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult, type DialogComponent } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; -import type { IAIProfileCredentialsDialogPayload } from './IAIProfileCredentialsDialogPayload.js'; import { AIProfilesResource } from '../AIProfilesResource.js'; -interface CredentialsDialogState { - token: string; - processing: boolean; +export interface IAIProfileCredentialsDialogPayload { + profileId: string; + profileName: string; + engineName: string; + engineIcon?: string; } export const AIProfileCredentialsDialog: DialogComponent = observer(function AIProfileCredentialsDialog({ @@ -46,26 +47,23 @@ export const AIProfileCredentialsDialog: DialogComponent({ autofocus: true }); - const state = useObservableRef( - () => ({ token: '', processing: false }), - { token: observable.ref, processing: observable.ref }, - false, - ); - const credentialsSaved = aiProfilesResource.get(payload.profileId)?.credentialsSaved ?? false; + const [token, setToken] = useState(''); + const [processing, setProcessing] = useState(false); + const credentialsSaved = aiProfilesResource.data?.credentialsSaved ?? false; const form = useForm({ onSubmit: save }); async function save(): Promise { - if (state.processing || !state.token) { + if (processing || !token) { return; } try { - state.processing = true; - await aiProfilesResource.saveCredentials(payload.profileId, state.token); + setProcessing(true); + await aiProfilesResource.resource.saveCredentials(payload.profileId, token); - state.token = ''; + setToken(''); notificationService.logSuccess({ title: 'plugin_ai_credentials_saved', message: payload.profileName, @@ -74,7 +72,7 @@ export const AIProfileCredentialsDialog: DialogComponent - + {translate('plugin_ai_credentials_profile')} - + {translate('plugin_ai_credentials_engine')} {translate('plugin_ai_credentials_token')} @@ -137,15 +136,15 @@ export const AIProfileCredentialsDialog: DialogComponent {credentialsSaved && ( - )} - - diff --git a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentials/AIProfileCredentialsService.ts b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentials/AIProfileCredentialsService.ts index 2156d786912..f0f0d7c27bc 100644 --- a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentials/AIProfileCredentialsService.ts +++ b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentials/AIProfileCredentialsService.ts @@ -12,7 +12,6 @@ import { NotificationService } from '@cloudbeaver/core-events'; import { AiEnginesResource } from '@cloudbeaver/plugin-ai'; import { AIProfileCredentialsDialog } from './AIProfileCredentialsDialogLazy.js'; -import { requiresUserCredentials, supportsUserCredentials } from './AIProfileCredentialsUtils.js'; import { AIProfilesResource, type AIProfile } from '../AIProfilesResource.js'; @injectable(() => [CommonDialogService, NotificationService, AIProfilesResource, AiEnginesResource]) @@ -25,14 +24,16 @@ export class AIProfileCredentialsService { ) {} async open(profileId: string): Promise> { - const [profile] = await Promise.all([this.aiProfilesResource.load(profileId), this.aiEnginesResource.load()]); + const profile = await this.aiProfilesResource.load(profileId); if (!profile) { this.notificationService.logError({ title: 'plugin_ai_credentials_profile_not_found' }); return { status: DialogueStateResult.Rejected }; } - const engine = this.aiEnginesResource.data.find(engine => engine.id === profile.engineId); + const engines = await this.aiEnginesResource.load(); + const engine = engines.find(engine => engine.id === profile.engineId); + return this.commonDialogService.open(AIProfileCredentialsDialog, { profileId: profile.id, profileName: profile.name, @@ -42,10 +43,10 @@ export class AIProfileCredentialsService { } isSupported(properties: ReadonlyArray<{ id?: string; features: readonly string[] }>): boolean { - return supportsUserCredentials(properties); + return properties.some(property => property.id === 'token' && property.features.includes('password')); } isRequired(profile: Pick): boolean { - return requiresUserCredentials(profile); + return !profile.global && !profile.credentialsSaved; } } diff --git a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentials/AIProfileCredentialsUtils.ts b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentials/AIProfileCredentialsUtils.ts deleted file mode 100644 index eec8a0ac22d..00000000000 --- a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentials/AIProfileCredentialsUtils.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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 { AIProfile } from '../AIProfilesResource.js'; - -export function supportsUserCredentials(properties: ReadonlyArray<{ id?: string; features: readonly string[] }>): boolean { - return properties.some(property => property.id === 'token' && property.features.includes('password')); -} - -export function requiresUserCredentials(profile: Pick): boolean { - return !profile.global && !profile.credentialsSaved; -} diff --git a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentials/IAIProfileCredentialsDialogPayload.ts b/webapp/packages/plugin-ai-profiles/src/AIProfileCredentials/IAIProfileCredentialsDialogPayload.ts deleted file mode 100644 index 281dbcdcf9a..00000000000 --- a/webapp/packages/plugin-ai-profiles/src/AIProfileCredentials/IAIProfileCredentialsDialogPayload.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * 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 interface IAIProfileCredentialsDialogPayload { - profileId: string; - profileName: string; - engineName: string; - engineIcon?: string; -} diff --git a/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.module.css b/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.module.css deleted file mode 100644 index 5f33db4ab0d..00000000000 --- a/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.module.css +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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. - */ - -.table { - font-size: 14px; - - & [role='columnheader'] { - text-transform: uppercase; - } - - :global(.rdg-cell) { - padding-inline: 12px; - } - - :global(.rdg-cell):not([role='columnheader']):not(:last-child) { - border-inline-end: 1px solid transparent; - } -} diff --git a/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.tsx b/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.tsx index 0aefa0d216d..fefba67db9b 100644 --- a/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.tsx +++ b/webapp/packages/plugin-ai-profiles/src/AIProfilesTable.tsx @@ -11,13 +11,13 @@ import { observer } from 'mobx-react-lite'; import type { ReactNode } from 'react'; import { IconOrImage, Link, s, TextPlaceholder, useResource, useS, useTranslate } from '@cloudbeaver/core-blocks'; +import { AdministrationTableStyles } from '@cloudbeaver/core-administration'; import { AiEnginesResource } from '@cloudbeaver/plugin-ai'; import { DataGrid, TableRowSelect, useCreateGridReactiveValue } from '@cloudbeaver/plugin-data-grid'; import { Command } from '@dbeaver/ui-kit'; import { AI_PROFILES_TABLE_ROW_HEIGHT } from './AI_PROFILES_TABLE_ROW_HEIGHT.js'; import type { AIProfile } from './AIProfilesResource.js'; -import AIProfilesTableStyles from './AIProfilesTable.module.css'; export interface IAIProfilesTableColumn { key: string; @@ -62,7 +62,7 @@ export const AIProfilesTable = observer(function AIProfil onProfileClick, }) { const translate = useTranslate(); - const styles = useS(AIProfilesTableStyles); + const styles = useS(AdministrationTableStyles); const enginesLoader = useResource(AIProfilesTable, AiEnginesResource, undefined); const selectable = !!isProfileSelectable; const columns: TableColumn[] = [ diff --git a/webapp/packages/plugin-ai-profiles/src/index.ts b/webapp/packages/plugin-ai-profiles/src/index.ts index 293fe23a800..fb89a81c000 100644 --- a/webapp/packages/plugin-ai-profiles/src/index.ts +++ b/webapp/packages/plugin-ai-profiles/src/index.ts @@ -9,8 +9,8 @@ import './module.js'; export * from './AIProfileCredentials/AIProfileCredentialsDialogLazy.js'; +export type { IAIProfileCredentialsDialogPayload } from './AIProfileCredentials/AIProfileCredentialsDialog.js'; export * from './AIProfileCredentials/AIProfileCredentialsService.js'; export * from './AIProfilesResource.js'; export { AIProfilesTable } from './AIProfilesTableLazy.js'; export type { IAIProfilesTableColumn, IAIProfilesTableProps } from './AIProfilesTable.js'; -export * from './AIProfileCredentials/IAIProfileCredentialsDialogPayload.js'; diff --git a/webapp/packages/plugin-ai-profiles/tsconfig.json b/webapp/packages/plugin-ai-profiles/tsconfig.json index b39b338b3ef..bb29136d70c 100644 --- a/webapp/packages/plugin-ai-profiles/tsconfig.json +++ b/webapp/packages/plugin-ai-profiles/tsconfig.json @@ -10,6 +10,9 @@ { "path": "../../common-react/@dbeaver/ui-kit" }, + { + "path": "../core-administration" + }, { "path": "../core-authentication" }, diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 76ef76bdcbc..ca3b348d692 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -2585,6 +2585,7 @@ __metadata: version: 0.0.0-use.local resolution: "@cloudbeaver/plugin-ai-profiles@workspace:packages/plugin-ai-profiles" dependencies: + "@cloudbeaver/core-administration": "workspace:*" "@cloudbeaver/core-authentication": "workspace:*" "@cloudbeaver/core-blocks": "workspace:*" "@cloudbeaver/core-cli": "workspace:*" @@ -2607,7 +2608,6 @@ __metadata: rimraf: "npm:^6" tslib: "npm:^2" typescript: "npm:^5" - typescript-plugin-css-modules: "npm:^5" languageName: unknown linkType: soft From c14e1a9422ce5584e235f3845a5919dd74129092 Mon Sep 17 00:00:00 2001 From: Ainur Date: Thu, 10 Sep 2026 17:51:28 +0200 Subject: [PATCH 26/31] dbeaver/pro#9532 Store embedded user secrets --- .../model/config/CBServerConfig.java | 11 +- .../cloudbeaver/server/CBApplicationCE.java | 14 + .../plugin.xml | 5 + .../security/CBSecretControllerEmbedded.java | 304 ++++++++++++++++++ .../CBSecretControllerEmbeddedTest.java | 86 +++++ .../test/platform/CEServerTestSuite.java | 1 + 6 files changed, 420 insertions(+), 1 deletion(-) create mode 100644 server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBSecretControllerEmbedded.java create mode 100644 server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CBSecretControllerEmbeddedTest.java diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/model/config/CBServerConfig.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/model/config/CBServerConfig.java index 176228957e3..5e507a4cbd4 100644 --- a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/model/config/CBServerConfig.java +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/model/config/CBServerConfig.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2025 DBeaver Corp and others + * 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. @@ -47,6 +47,7 @@ public class CBServerConfig implements WebServerConfiguration { private long maxSessionIdleTime = CBAuthConstants.MAX_SESSION_IDLE_TIME; private boolean develMode = false; private boolean enableSecurityManager = false; + protected String secretController = "cb-embedded"; private final Map productSettings = new HashMap<>(); @SerializedName("sm") @@ -147,6 +148,14 @@ public void setEnableSecurityManager(boolean enableSecurityManager) { this.enableSecurityManager = enableSecurityManager; } + public String getSecretControllerId() { + return secretController; + } + + public void setSecretControllerId(String secretControllerId) { + this.secretController = secretControllerId; + } + public void setDatabaseConfiguration(WebDatabaseConfig databaseConfiguration) { this.databaseConfiguration = databaseConfiguration; } diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBApplicationCE.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBApplicationCE.java index a8f21101996..6c4edd07726 100644 --- a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBApplicationCE.java +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBApplicationCE.java @@ -29,7 +29,10 @@ import org.jkiss.dbeaver.model.app.DBPWorkspace; import org.jkiss.dbeaver.model.auth.SMAuthConfiguration; import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; +import org.jkiss.dbeaver.model.auth.SMSessionContext; import org.jkiss.dbeaver.model.rm.RMController; +import org.jkiss.dbeaver.model.secret.DBSSecretController; +import org.jkiss.dbeaver.model.secret.SecretControllerRegistry; import org.jkiss.dbeaver.model.security.SMAdminController; import org.jkiss.dbeaver.model.security.SMController; import org.jkiss.dbeaver.registry.LocalFileController; @@ -75,6 +78,17 @@ protected SMAdminController createGlobalSecurityController() throws DBException ); } + @NotNull + @Override + public DBSSecretController getSecretController( + @NotNull SMCredentialsProvider credentialsProvider, + SMSessionContext smSessionContext + ) throws DBException { + return SecretControllerRegistry.getInstance().getAuthorizedSecretController( + getServerConfiguration().getSecretControllerId(), credentialsProvider, smSessionContext + ); + } + @NotNull @Override public RMController createResourceController( diff --git a/server/bundles/io.cloudbeaver.service.security/plugin.xml b/server/bundles/io.cloudbeaver.service.security/plugin.xml index e29aa46c071..338f21b3c26 100644 --- a/server/bundles/io.cloudbeaver.service.security/plugin.xml +++ b/server/bundles/io.cloudbeaver.service.security/plugin.xml @@ -2,6 +2,11 @@ + + + + 0) { + return; + } + } + try (PreparedStatement dbStat = dbCon.prepareStatement( + "INSERT INTO {table_prefix}CB_SUBJECT_SECRETS" + + "(SUBJECT_ID,SECRET_ID,SECRET_VALUE,ENCODING_TYPE) VALUES(?,?,?,?)") + ) { + dbStat.setString(1, userId); + dbStat.setString(2, secretId); + dbStat.setString(3, encodedSecretValue.value()); + dbStat.setString(4, encodedSecretValue.encodingType()); + dbStat.executeUpdate(); + } + } catch (SQLException e) { + throw new DBException("Error saving secret value", e); + } + } + + @Override + public void setPrivateSecretValue(@NotNull DBSSecretObject secretObject, @NotNull DBSSecretValue secretValue) + throws DBException { + setSubjectSecretValue(getCurrentUserId(), secretObject, secretValue); + } + + @Override + public void setSubjectSecretValue( + @NotNull String subjectId, + @NotNull DBSSecretObject secretObject, + @NotNull DBSSecretValue secretValue + ) throws DBException { + if (secretValue.getSubjectId() != null && !subjectId.equals(secretValue.getSubjectId())) { + throw new DBException("Subject id mismatch"); + } + try (Connection dbCon = getDatabase().openConnection()) { + if (!isSubjectSupportsSecrets(dbCon, subjectId)) { + throw new DBException("Subject does not support secrets"); + } + if (secretValue.getValue() == null) { + deleteSecretValue(subjectId, secretValue.getId()); + return; + } + EncodedValue encodedSecretValue = encodeSecretValue(secretValue.getValue()); + try (PreparedStatement dbStat = dbCon.prepareStatement( + "UPDATE {table_prefix}CB_SUBJECT_SECRETS " + + "SET SECRET_VALUE=?,ENCODING_TYPE=?,PROJECT_ID=?,OBJECT_ID=?,OBJECT_TYPE=? " + + "WHERE SUBJECT_ID=? AND SECRET_ID=?" + )) { + dbStat.setString(1, encodedSecretValue.value()); + dbStat.setString(2, encodedSecretValue.encodingType()); + dbStat.setString(3, secretObject.getProjectId()); + dbStat.setString(4, secretObject.getSecretObjectId()); + dbStat.setString(5, secretObject.getSecretObjectType()); + dbStat.setString(6, subjectId); + dbStat.setString(7, secretValue.getId()); + if (dbStat.executeUpdate() > 0) { + return; + } + } + try (PreparedStatement dbStat = dbCon.prepareStatement( + "INSERT INTO {table_prefix}CB_SUBJECT_SECRETS" + + "(SUBJECT_ID,SECRET_ID,SECRET_VALUE,ENCODING_TYPE,PROJECT_ID,OBJECT_ID,OBJECT_TYPE) " + + "VALUES(?,?,?,?,?,?,?)") + ) { + dbStat.setString(1, subjectId); + dbStat.setString(2, secretValue.getId()); + dbStat.setString(3, encodedSecretValue.value()); + dbStat.setString(4, encodedSecretValue.encodingType()); + dbStat.setString(5, secretObject.getProjectId()); + dbStat.setString(6, secretObject.getSecretObjectId()); + dbStat.setString(7, secretObject.getSecretObjectType()); + dbStat.executeUpdate(); + } + } catch (SQLException e) { + throw new DBException("Error saving secret value", e); + } + } + + @Override + public void deleteObjectSecrets(@NotNull DBSSecretObject secretObject) throws DBException { + log.info("Delete all object secrets " + String.join(":", secretObject.getSecretObjectId(), + secretObject.getSecretObjectType(), secretObject.getProjectId()) + ); + try (var dbCon = getDatabase().openConnection()) { + JDBCUtils.executeStatement( + dbCon, + "DELETE FROM {table_prefix}CB_SUBJECT_SECRETS " + + "WHERE PROJECT_ID=? AND OBJECT_TYPE=? AND OBJECT_ID=?", + secretObject.getProjectId(), + secretObject.getSecretObjectType(), + secretObject.getSecretObjectId() + ); + } catch (SQLException e) { + throw new DBException("Error deleting secrets from database", e); + } + } + + @Override + public void deleteSubjectSecrets(@NotNull String subjectId) throws DBException { + try (var dbCon = getDatabase().openConnection()) { + JDBCUtils.executeStatement( + dbCon, + "DELETE FROM {table_prefix}CB_SUBJECT_SECRETS WHERE SUBJECT_ID=?", + subjectId + ); + } catch (SQLException e) { + throw new DBException("Error deleting secrets from database", e); + } + } + + @Override + public void deleteProjectSecrets(@NotNull String projectId) throws DBException { + try (var dbCon = getDatabase().openConnection()) { + JDBCUtils.executeStatement( + dbCon, + "DELETE FROM {table_prefix}CB_SUBJECT_SECRETS WHERE PROJECT_ID=?", + projectId + ); + } catch (SQLException e) { + throw new DBException("Error deleting secrets from database", e); + } + } + + @NotNull + @Override + public List discoverCurrentUserSecrets(@NotNull DBSSecretObject secretObject) throws DBException { + throw new DBCFeatureNotSupportedException("Secrets discovery not supported"); + } + + @Override + public void flushChanges() throws DBException { + } + + @Override + public void authorize( + @Nullable SMCredentialsProvider credentialsProvider, + @Nullable SMSessionContext smSessionContext + ) { + this.credentialsProvider = credentialsProvider; + } + + @NotNull + protected SMCredentialsProvider getCredentialsProvider() throws DBException { + if (credentialsProvider == null) { + throw new DBException("Secret controller is not authorized"); + } + return credentialsProvider; + } + + @NotNull + protected String getCurrentUserId() throws DBException { + var credentials = getCredentialsProvider().getActiveUserCredentials(); + if (credentials == null || CommonUtils.isEmpty(credentials.getUserId())) { + throw new DBException("Empty user id"); + } + return credentials.getUserId(); + } + + protected void deleteSecretValue(@NotNull String subjectId, @NotNull String secretId) throws DBException { + try (Connection dbCon = getDatabase().openConnection(); + PreparedStatement dbStat = dbCon.prepareStatement( + "DELETE FROM {table_prefix}CB_SUBJECT_SECRETS WHERE SUBJECT_ID=? AND SECRET_ID=?" + )) { + dbStat.setString(1, subjectId); + dbStat.setString(2, secretId); + dbStat.executeUpdate(); + } catch (SQLException e) { + throw new DBException("Error deleting secret value", e); + } + } + + protected boolean isSubjectSupportsSecrets(@NotNull Connection dbCon, @NotNull String subjectId) + throws SQLException, DBException { + try (PreparedStatement dbStat = dbCon.prepareStatement( + "SELECT IS_SECRET_STORAGE FROM {table_prefix}CB_AUTH_SUBJECT WHERE SUBJECT_ID=?") + ) { + dbStat.setString(1, subjectId); + try (ResultSet dbResult = dbStat.executeQuery()) { + if (!dbResult.next()) { + throw new DBException("Subject not exists: " + subjectId); + } + return CBEmbeddedSecurityController.stringToBoolean(dbResult.getString(1)); + } + } + } + + @NotNull + protected EncodedValue encodeSecretValue(@NotNull String value) throws DBException { + return new EncodedValue(value, ENCODING_PLAINTEXT); + } + + @NotNull + protected String decodeSecretValue(@NotNull String value, @NotNull String encodingType) throws DBException { + if (!ENCODING_PLAINTEXT.equals(encodingType)) { + throw new DBException("Unsupported secret encoding: " + encodingType); + } + return value; + } + + @NotNull + protected CBDatabase getDatabase() throws DBException { + CBDatabase database = EmbeddedSecurityControllerFactory.getDbInstance(); + if (database == null) { + throw new DBException("Embedded database is not initialized"); + } + return database; + } + + protected record EncodedValue(@NotNull String value, @NotNull String encodingType) { + public EncodedValue { + } + } +} diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CBSecretControllerEmbeddedTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CBSecretControllerEmbeddedTest.java new file mode 100644 index 00000000000..0faffe41bd5 --- /dev/null +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CBSecretControllerEmbeddedTest.java @@ -0,0 +1,86 @@ +/* + * 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; + +import io.cloudbeaver.app.CEAppStarter; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.auth.SMCredentials; +import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; +import org.jkiss.dbeaver.model.secret.DBSSecretController; +import org.jkiss.dbeaver.model.secret.DBSSecretObject; +import org.jkiss.dbeaver.model.secret.DBSSecretValue; +import org.jkiss.dbeaver.runtime.DBWorkbench; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +public class CBSecretControllerEmbeddedTest { + private static final String TEST_USER_ID = "test"; + private static final DBSSecretObject TEST_OBJECT = new DBSSecretObject() { + @Override + public String getProjectId() { + return "test-project"; + } + + @Override + public String getSecretObjectId() { + return "test-object"; + } + + @Override + public String getSecretObjectType() { + return "test-type"; + } + }; + + @Test + public void testPrivateSecretsPersistBetweenControllers() throws DBException { + String secretId = "ce_private_secret_test"; + DBSSecretController firstController = createController(TEST_USER_ID); + DBSSecretController secondController = createController(TEST_USER_ID); + DBSSecretController anotherUserController = createController("another-user"); + + try { + firstController.setPrivateSecretValue( + TEST_OBJECT, + new DBSSecretValue(secretId, "Test secret", "secret-value") + ); + + Assertions.assertEquals("secret-value", secondController.getPrivateSecretValue(secretId)); + Assertions.assertNull(anotherUserController.getPrivateSecretValue(secretId)); + + secondController.deleteObjectSecrets(TEST_OBJECT); + Assertions.assertNull(firstController.getPrivateSecretValue(secretId)); + } finally { + firstController.setPrivateSecretValue(secretId, null); + } + } + + private static DBSSecretController createController(String userId) throws DBException { + SMCredentialsProvider credentialsProvider = () -> new SMCredentials( + "test-token", + userId, + "test-session", + Set.of() + ); + return CEAppStarter.getTestApp().getSecretController( + credentialsProvider, + DBWorkbench.getPlatform().getWorkspace().getAuthContext() + ); + } +} diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java index c668bbdae0c..22e0540a22c 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java @@ -48,6 +48,7 @@ LocalResourceControllerTest.class, NoSessionTest.class, FileSystemSecurityTest.class, + CBSecretControllerEmbeddedTest.class, WebSessionTest.class, WebSessionProjectTest.class, WSEventHandlerWorkspaceConfigUpdateTest.class, From b9af5a6c5b5f4f12891fc5a5f18f3e5fd8687e93 Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Fri, 11 Sep 2026 11:32:09 +0200 Subject: [PATCH 27/31] dbeaver/pro#9532 Move AI credentials handlers into chat service --- .../AIChatMessage/AIChatMessageService.ts | 70 ++++++++++++++----- .../AIChatProfileCredentialsBootstrap.ts | 44 ------------ webapp/packages/plugin-ai-chat/src/module.ts | 2 - 3 files changed, 52 insertions(+), 64 deletions(-) delete mode 100644 webapp/packages/plugin-ai-chat/src/AIChat/AIChatProfileCredentialsBootstrap.ts diff --git a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/AIChatMessageService.ts b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/AIChatMessageService.ts index 5a96bef73df..984b534f745 100644 --- a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/AIChatMessageService.ts +++ b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/AIChatMessageService.ts @@ -12,9 +12,10 @@ import { injectable } from '@cloudbeaver/core-di'; import { ConfirmationDialog } from '@cloudbeaver/core-blocks'; import { DialogueStateResult, CommonDialogService } from '@cloudbeaver/core-dialogs'; import { LocalizationService } from '@cloudbeaver/core-localization'; -import { Executor, ExecutorInterrupter } from '@cloudbeaver/core-executor'; +import { Executor, ExecutorInterrupter, type IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { ConnectionsManagerService, type IConnectionInfoParams } from '@cloudbeaver/core-connections'; import type { AiSendChatMessageInfoFragment } from '@cloudbeaver/core-sdk'; +import { AIProfileCredentialsService, AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; import { AIChatMessagesResource, isFunctionConfirmationMessage, isFunctionMessage, type IMessageParam } from './AIChatMessagesResource.js'; import { AIChatConversationsResource } from '../AIChatConversation/AIChatConversationsResource.js'; @@ -44,7 +45,15 @@ interface IMessageSendExecutorAfterData { type MessageSendExecutorData = IMessageSendExecutorBeforeData | IMessageSendExecutorAfterData; -@injectable(() => [AIChatMessagesResource, AIChatConversationsResource, CommonDialogService, LocalizationService, ConnectionsManagerService]) +@injectable(() => [ + AIChatMessagesResource, + AIChatConversationsResource, + CommonDialogService, + LocalizationService, + ConnectionsManagerService, + AIProfilesResource, + AIProfileCredentialsService, +]) export class AIChatMessageService { onMessageSend: Executor; @@ -54,25 +63,13 @@ export class AIChatMessageService { private readonly commonDialogService: CommonDialogService, private readonly localizationService: LocalizationService, private readonly connectionsManagerService: ConnectionsManagerService, + private readonly aiProfilesResource: AIProfilesResource, + private readonly credentialsService: AIProfileCredentialsService, ) { this.onMessageSend = new Executor(); - this.onMessageSend.addHandler(({ stage, data }) => { - if (stage === 'after') { - const conversation = this.aiChatConversationsResource.get(data.conversation.id); - - if (conversation) { - runInAction(() => { - if (conversation.caption !== data.conversation.caption) { - conversation.caption = data.conversation.caption; - } - - conversation.time = data.conversation.time; - conversation.waitingForResponse = data.conversation.waitingForResponse; - }); - } - } - }); + this.onMessageSend.addHandler(this.checkProfileCredentials.bind(this)); + this.onMessageSend.addHandler(this.updateConversation.bind(this)); } async sendMessage(conversationId: string, prompt: string) { @@ -157,4 +154,41 @@ export class AIChatMessageService { return message; } + + private async checkProfileCredentials( + { stage, data }: MessageSendExecutorData, + contexts: IExecutionContextProvider, + ): Promise { + if (stage !== 'before') { + return; + } + + const conversation = await this.aiChatConversationsResource.load(data.conversationId); + const profile = conversation.profile ? await this.aiProfilesResource.load(conversation.profile) : undefined; + + if (profile && this.credentialsService.isRequired(profile)) { + const { status } = await this.credentialsService.open(profile.id); + + if (status !== DialogueStateResult.Resolved) { + ExecutorInterrupter.interrupt(contexts); + } + } + } + + private updateConversation({ stage, data }: MessageSendExecutorData): void { + if (stage === 'after') { + const conversation = this.aiChatConversationsResource.get(data.conversation.id); + + if (conversation) { + runInAction(() => { + if (conversation.caption !== data.conversation.caption) { + conversation.caption = data.conversation.caption; + } + + conversation.time = data.conversation.time; + conversation.waitingForResponse = data.conversation.waitingForResponse; + }); + } + } + } } diff --git a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatProfileCredentialsBootstrap.ts b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatProfileCredentialsBootstrap.ts deleted file mode 100644 index c77de443e33..00000000000 --- a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatProfileCredentialsBootstrap.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { DialogueStateResult } from '@cloudbeaver/core-dialogs'; -import { ExecutorInterrupter } from '@cloudbeaver/core-executor'; -import { AIProfileCredentialsService, AIProfilesResource } from '@cloudbeaver/plugin-ai-profiles'; - -import { AIChatConversationsResource } from './AIChatConversation/AIChatConversationsResource.js'; -import { AIChatMessageService } from './AIChatMessage/AIChatMessageService.js'; - -@injectable(() => [AIChatMessageService, AIChatConversationsResource, AIProfilesResource, AIProfileCredentialsService]) -export class AIChatProfileCredentialsBootstrap extends Bootstrap { - constructor( - aiChatMessageService: AIChatMessageService, - aiChatConversationsResource: AIChatConversationsResource, - aiProfilesResource: AIProfilesResource, - credentialsService: AIProfileCredentialsService, - ) { - super(); - - aiChatMessageService.onMessageSend.addHandler(async (event, contexts) => { - if (event.stage !== 'before') { - return; - } - - const conversation = await aiChatConversationsResource.load(event.data.conversationId); - const profile = conversation.profile ? await aiProfilesResource.load(conversation.profile) : undefined; - - if (profile && credentialsService.isRequired(profile)) { - const { status } = await credentialsService.open(profile.id); - - if (status !== DialogueStateResult.Resolved) { - ExecutorInterrupter.interrupt(contexts); - } - } - }); - } -} diff --git a/webapp/packages/plugin-ai-chat/src/module.ts b/webapp/packages/plugin-ai-chat/src/module.ts index d4938ef082c..99563334228 100644 --- a/webapp/packages/plugin-ai-chat/src/module.ts +++ b/webapp/packages/plugin-ai-chat/src/module.ts @@ -23,7 +23,6 @@ import { AIChatConversationScopeResource } from './AIChat/AIChatConversation/AIC import { AIChatConversationMetricsResource } from './AIChat/AIChatConversation/AIChatConversationMetricsResource.js'; import { AIChatFunctionsService } from './AIChatFunctionsService.js'; import { AIFunctionsResource } from './AIFunctionsResource.js'; -import { AIChatProfileCredentialsBootstrap } from './AIChat/AIChatProfileCredentialsBootstrap.js'; export default ModuleRegistry.add({ name: '@cloudbeaver/plugin-ai-chat', @@ -32,7 +31,6 @@ export default ModuleRegistry.add({ serviceCollection .addSingleton(Bootstrap, LocaleService) .addSingleton(Bootstrap, AIChatServiceBootstrap) - .addSingleton(Bootstrap, AIChatProfileCredentialsBootstrap) .addSingleton(Bootstrap, proxy(AIChatContextService)) .addSingleton(Dependency, proxy(AIChatSettingsService)) .addSingleton(Dependency, proxy(AIChatMessagesResource)) From 6b0c6cf45d9f31fc0d5fe2b98cd619b2e98c43f3 Mon Sep 17 00:00:00 2001 From: Ainur Date: Fri, 11 Sep 2026 15:58:09 +0200 Subject: [PATCH 28/31] dbeaver/pro#9532 Address backend review feedback --- .../model/config/CBServerConfig.java | 4 +++- .../cloudbeaver/server/CBApplicationCE.java | 2 +- ...redentials.java => WebAIProfileUtils.java} | 4 ++-- .../io/cloudbeaver/service/ai/WebAIUtils.java | 4 ++-- .../service/ai/gql/WebServiceAI.java | 20 ++++++++-------- .../ai/model/WebAIConfigurationProfile.java | 4 ++-- .../security/CBSecretControllerEmbedded.java | 1 + ...lsTest.java => WebAIProfileUtilsTest.java} | 24 +++++++++---------- .../CBSecretControllerEmbeddedTest.java | 7 +++++- .../test/platform/CEServerTestSuite.java | 4 ++-- 10 files changed, 41 insertions(+), 33 deletions(-) rename server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/{WebAIProfileCredentials.java => WebAIProfileUtils.java} (99%) rename server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/service/ai/{WebAIProfileCredentialsTest.java => WebAIProfileUtilsTest.java} (82%) diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/model/config/CBServerConfig.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/model/config/CBServerConfig.java index 5e507a4cbd4..8e0f46615c6 100644 --- a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/model/config/CBServerConfig.java +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/model/config/CBServerConfig.java @@ -47,6 +47,7 @@ public class CBServerConfig implements WebServerConfiguration { private long maxSessionIdleTime = CBAuthConstants.MAX_SESSION_IDLE_TIME; private boolean develMode = false; private boolean enableSecurityManager = false; + @NotNull protected String secretController = "cb-embedded"; private final Map productSettings = new HashMap<>(); @@ -148,11 +149,12 @@ public void setEnableSecurityManager(boolean enableSecurityManager) { this.enableSecurityManager = enableSecurityManager; } + @NotNull public String getSecretControllerId() { return secretController; } - public void setSecretControllerId(String secretControllerId) { + public void setSecretControllerId(@NotNull String secretControllerId) { this.secretController = secretControllerId; } diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBApplicationCE.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBApplicationCE.java index 6c4edd07726..caa03d54fc8 100644 --- a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBApplicationCE.java +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBApplicationCE.java @@ -82,7 +82,7 @@ protected SMAdminController createGlobalSecurityController() throws DBException @Override public DBSSecretController getSecretController( @NotNull SMCredentialsProvider credentialsProvider, - SMSessionContext smSessionContext + @Nullable SMSessionContext smSessionContext ) throws DBException { return SecretControllerRegistry.getInstance().getAuthorizedSecretController( getServerConfiguration().getSecretControllerId(), credentialsProvider, smSessionContext diff --git a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIProfileCredentials.java b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIProfileUtils.java similarity index 99% rename from server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIProfileCredentials.java rename to server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIProfileUtils.java index 9a782e5f302..ef587064e8d 100644 --- a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIProfileCredentials.java +++ b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIProfileUtils.java @@ -37,12 +37,12 @@ import java.util.Map; import java.util.Set; -public final class WebAIProfileCredentials { +public final class WebAIProfileUtils { private static final String SECRET_ID_PREFIX = "ai.profile."; private static final String SECRET_OBJECT_TYPE = "aiProfile"; private static final String SESSION_CREDENTIALS_ATTRIBUTE_PREFIX = "ai.profile.credentials."; - private WebAIProfileCredentials() { + private WebAIProfileUtils() { } public static boolean areCredentialsSaved( diff --git a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIUtils.java b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIUtils.java index 7c4c103a9dc..e13ae11cdf1 100644 --- a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIUtils.java +++ b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIUtils.java @@ -151,7 +151,7 @@ protected IStatus run(@NotNull DBRProgressMonitor monitor) { if (selectedProfile == null) { selectedProfile = AISettingsManager.getStaticSettings().getDefaultConfiguration(); } - AIConfigurationProfile effectiveProfile = WebAIProfileCredentials.getEffectiveProfile( + AIConfigurationProfile effectiveProfile = WebAIProfileUtils.getEffectiveProfile( webSession, selectedProfile ); @@ -286,7 +286,7 @@ public static WebAISendChatMessageInfo submitPrompt( if (selectedProfile == null) { selectedProfile = AISettingsManager.getStaticSettings().getDefaultConfiguration(); } - AIConfigurationProfile effectiveProfile = WebAIProfileCredentials.getEffectiveProfile(webSession, selectedProfile); + AIConfigurationProfile effectiveProfile = WebAIProfileUtils.getEffectiveProfile(webSession, selectedProfile); if (!effectiveProfile.getConfiguration().isValidConfiguration()) { throw new DBWebException("Invalid AI configuration"); } diff --git a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/gql/WebServiceAI.java b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/gql/WebServiceAI.java index 6429ff37e2d..61c27212c5e 100644 --- a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/gql/WebServiceAI.java +++ b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/gql/WebServiceAI.java @@ -23,7 +23,7 @@ import io.cloudbeaver.model.session.WebAsyncTaskProcessor; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.server.CBApplication; -import io.cloudbeaver.service.ai.WebAIProfileCredentials; +import io.cloudbeaver.service.ai.WebAIProfileUtils; import io.cloudbeaver.service.ai.WebAIUtils; import io.cloudbeaver.service.ai.model.*; import io.cloudbeaver.service.ai.model.events.WSAiChatMessageEvent; @@ -240,7 +240,7 @@ public boolean saveEngineConfiguration( AISettings settings = AISettingsManager.getInstance().getSettings(); AIConfigurationProfile profile = settings.getConfiguration(profileId); profile.setConfiguration(toEngineConfiguration(webSession.getProgressMonitor(), profile, engineSettingsInput)); - WebAIProfileCredentials.prepareGlobalProfile(webSession, profile); + WebAIProfileUtils.prepareGlobalProfile(webSession, profile); AISettingsManager.getInstance().saveSettings(); addAISettingsChangedEvent(webSession); return true; @@ -274,7 +274,7 @@ public void run(DBRProgressMonitor monitor) throws InvocationTargetException { .build(); AIAssistant assistant = AIAssistantRegistry.getInstance().getAssistant(webSession.getWorkspace()); - AIConfigurationProfile profile = WebAIProfileCredentials.getEffectiveProfile( + AIConfigurationProfile profile = WebAIProfileUtils.getEffectiveProfile( webSession, AISettingsManager.getStaticSettings().getDefaultConfiguration() ); @@ -555,9 +555,9 @@ public WebAIConfigurationProfile createProfile( profile.setConfiguration(toEngineConfiguration(webSession.getProgressMonitor(), profile, input.configuration())); } if (!profile.isGlobal()) { - WebAIProfileCredentials.validateCredentialsSupport(webSession, profile.getConfiguration()); + WebAIProfileUtils.validateCredentialsSupport(webSession, profile.getConfiguration()); } - WebAIProfileCredentials.prepareGlobalProfile(webSession, profile); + WebAIProfileUtils.prepareGlobalProfile(webSession, profile); AISettingsManager.getInstance().saveSettings(); addAISettingsChangedEvent(webSession); return new WebAIConfigurationProfile(webSession, settings.getConfiguration(input.profileId())); @@ -585,12 +585,12 @@ public WebAIConfigurationProfile updateProfile( profile.setConfiguration(toEngineConfiguration(webSession.getProgressMonitor(), profile, input.configuration())); } if (!wasGlobal && profile.isGlobal()) { - WebAIProfileCredentials.deleteCredentials(webSession, profile); + WebAIProfileUtils.deleteCredentials(webSession, profile); } if (!profile.isGlobal()) { - WebAIProfileCredentials.validateCredentialsSupport(webSession, profile.getConfiguration()); + WebAIProfileUtils.validateCredentialsSupport(webSession, profile.getConfiguration()); } - WebAIProfileCredentials.prepareGlobalProfile(webSession, profile); + WebAIProfileUtils.prepareGlobalProfile(webSession, profile); AISettingsManager.getInstance().saveSettings(); addAISettingsChangedEvent(webSession); return new WebAIConfigurationProfile(webSession, profile); @@ -605,7 +605,7 @@ public boolean deleteProfile(@NotNull WebSession webSession, @NotNull String pro try { AISettings settings = AISettingsManager.getInstance().getSettings(); AIConfigurationProfile profile = settings.getConfiguration(profileId); - WebAIProfileCredentials.deleteCredentials(webSession, profile); + WebAIProfileUtils.deleteCredentials(webSession, profile); settings.removeConfiguration(profile); AISettingsManager.getInstance().saveSettings(); addAISettingsChangedEvent(webSession); @@ -624,7 +624,7 @@ public boolean saveProfileCredentials( WebAIUtils.validateAiPluginEnabled(); try { AIConfigurationProfile profile = AISettingsManager.getInstance().getSettings().getConfiguration(profileId); - WebAIProfileCredentials.saveCredentials(webSession, profile, credentials.properties()); + WebAIProfileUtils.saveCredentials(webSession, profile, credentials.properties()); return true; } catch (DBException e) { throw new DBWebException("Error saving credentials for AI profile " + profileId, e); diff --git a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/model/WebAIConfigurationProfile.java b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/model/WebAIConfigurationProfile.java index d29684cd77f..a49b5831c7d 100644 --- a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/model/WebAIConfigurationProfile.java +++ b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/model/WebAIConfigurationProfile.java @@ -19,7 +19,7 @@ import io.cloudbeaver.WebServiceUtils; import io.cloudbeaver.model.WebPropertyInfo; import io.cloudbeaver.model.session.WebSession; -import io.cloudbeaver.service.ai.WebAIProfileCredentials; +import io.cloudbeaver.service.ai.WebAIProfileUtils; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.ai.AIConfigurationProfile; @@ -56,7 +56,7 @@ public boolean isGlobal() { } public boolean isCredentialsSaved() throws DBException { - return WebAIProfileCredentials.areCredentialsSaved(webSession, profile); + return WebAIProfileUtils.areCredentialsSaved(webSession, profile); } @NotNull diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBSecretControllerEmbedded.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBSecretControllerEmbedded.java index 8e0adc8cc59..0c3f6ead5d7 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBSecretControllerEmbedded.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBSecretControllerEmbedded.java @@ -43,6 +43,7 @@ public class CBSecretControllerEmbedded implements DBSSecretControllerAuthorized private static final Log log = Log.getLog(CBSecretControllerEmbedded.class); private static final String ENCODING_PLAINTEXT = "PLAINTEXT"; + @Nullable private SMCredentialsProvider credentialsProvider; @Override diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/service/ai/WebAIProfileCredentialsTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/service/ai/WebAIProfileUtilsTest.java similarity index 82% rename from server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/service/ai/WebAIProfileCredentialsTest.java rename to server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/service/ai/WebAIProfileUtilsTest.java index e2b1e0c5425..25bb97fa593 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/service/ai/WebAIProfileCredentialsTest.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/service/ai/WebAIProfileUtilsTest.java @@ -33,7 +33,7 @@ import java.util.HashMap; import java.util.Map; -public class WebAIProfileCredentialsTest { +public class WebAIProfileUtilsTest { private final Map secrets = new HashMap<>(); private final Map sessionAttributes = new HashMap<>(); private DBSSecretController secretController; @@ -102,22 +102,22 @@ public void setUp() throws DBException { @Test public void savesUpdatesAndClearsCredentials() throws DBException { - WebAIProfileCredentials.saveCredentials(webSession, profile, Map.of(credentialPropertyId, "first")); - Assertions.assertTrue(WebAIProfileCredentials.areCredentialsSaved(webSession, profile)); + WebAIProfileUtils.saveCredentials(webSession, profile, Map.of(credentialPropertyId, "first")); + Assertions.assertTrue(WebAIProfileUtils.areCredentialsSaved(webSession, profile)); - WebAIProfileCredentials.saveCredentials(webSession, profile, Map.of(credentialPropertyId, "updated")); + WebAIProfileUtils.saveCredentials(webSession, profile, Map.of(credentialPropertyId, "updated")); Assertions.assertTrue(secrets.containsValue("updated")); Assertions.assertFalse(secrets.containsValue("first")); - WebAIProfileCredentials.saveCredentials(webSession, profile, Map.of(credentialPropertyId, "")); - Assertions.assertFalse(WebAIProfileCredentials.areCredentialsSaved(webSession, profile)); + WebAIProfileUtils.saveCredentials(webSession, profile, Map.of(credentialPropertyId, "")); + Assertions.assertFalse(WebAIProfileUtils.areCredentialsSaved(webSession, profile)); } @Test public void rejectsNonCredentialProperties() { Assertions.assertThrows( DBException.class, - () -> WebAIProfileCredentials.saveCredentials(webSession, profile, Map.of("model", "invalid")) + () -> WebAIProfileUtils.saveCredentials(webSession, profile, Map.of("model", "invalid")) ); } @@ -125,7 +125,7 @@ public void rejectsNonCredentialProperties() { public void removesCredentialsFromNonGlobalConfiguration() throws DBException { properties.setToken("global-token"); - WebAIProfileCredentials.prepareGlobalProfile(webSession, profile); + WebAIProfileUtils.prepareGlobalProfile(webSession, profile); Assertions.assertNull(properties.getToken()); } @@ -134,12 +134,12 @@ public void removesCredentialsFromNonGlobalConfiguration() throws DBException { public void storesCredentialsInSessionWithoutPrivateSecretStorage() throws DBException { Mockito.when(secretController.getSupportedFeatures()).thenReturn(0L); - WebAIProfileCredentials.saveCredentials(webSession, profile, Map.of(credentialPropertyId, "session-token")); + WebAIProfileUtils.saveCredentials(webSession, profile, Map.of(credentialPropertyId, "session-token")); - Assertions.assertTrue(WebAIProfileCredentials.areCredentialsSaved(webSession, profile)); + Assertions.assertTrue(WebAIProfileUtils.areCredentialsSaved(webSession, profile)); Assertions.assertTrue(secrets.isEmpty()); - WebAIProfileCredentials.saveCredentials(webSession, profile, Map.of(credentialPropertyId, "")); - Assertions.assertFalse(WebAIProfileCredentials.areCredentialsSaved(webSession, profile)); + WebAIProfileUtils.saveCredentials(webSession, profile, Map.of(credentialPropertyId, "")); + Assertions.assertFalse(WebAIProfileUtils.areCredentialsSaved(webSession, profile)); } } diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CBSecretControllerEmbeddedTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CBSecretControllerEmbeddedTest.java index 0faffe41bd5..0cd493ad8c3 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CBSecretControllerEmbeddedTest.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CBSecretControllerEmbeddedTest.java @@ -17,6 +17,7 @@ package io.cloudbeaver.test.platform; import io.cloudbeaver.app.CEAppStarter; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.auth.SMCredentials; import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; @@ -32,16 +33,19 @@ public class CBSecretControllerEmbeddedTest { private static final String TEST_USER_ID = "test"; private static final DBSSecretObject TEST_OBJECT = new DBSSecretObject() { + @NotNull @Override public String getProjectId() { return "test-project"; } + @NotNull @Override public String getSecretObjectId() { return "test-object"; } + @NotNull @Override public String getSecretObjectType() { return "test-type"; @@ -71,7 +75,8 @@ public void testPrivateSecretsPersistBetweenControllers() throws DBException { } } - private static DBSSecretController createController(String userId) throws DBException { + @NotNull + private static DBSSecretController createController(@NotNull String userId) throws DBException { SMCredentialsProvider credentialsProvider = () -> new SMCredentials( "test-token", userId, diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java index 22e0540a22c..f2d892fcefb 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java @@ -24,7 +24,7 @@ import io.cloudbeaver.model.rm.lock.RMLockTest; import io.cloudbeaver.model.session.WebSessionProjectTest; import io.cloudbeaver.model.session.WebSessionTest; -import io.cloudbeaver.service.ai.WebAIProfileCredentialsTest; +import io.cloudbeaver.service.ai.WebAIProfileUtilsTest; import io.cloudbeaver.server.events.WSEventHandlerWorkspaceConfigUpdateTest; import io.cloudbeaver.test.platform.admin.AdminCreateUserTest; import io.cloudbeaver.test.platform.admin.AdminImportUsersTest; @@ -53,7 +53,7 @@ WebSessionProjectTest.class, WSEventHandlerWorkspaceConfigUpdateTest.class, WebNavigatorNodeInfoTest.class, - WebAIProfileCredentialsTest.class, + WebAIProfileUtilsTest.class, AdminCreateUserTest.class, AdminImportUsersTest.class, AdminLastLoginTimeTest.class, From 0b6298e9d78c6b7c70583d6bf196774aaed80b6e Mon Sep 17 00:00:00 2001 From: Ainur Date: Fri, 11 Sep 2026 16:55:17 +0200 Subject: [PATCH 29/31] dbeaver/pro#9532 Secure user credential fallback --- .../io/cloudbeaver/model/user/WebUser.java | 4 + .../service/ai/WebAIProfileUtils.java | 62 +++++++++++---- .../security/CBSecretControllerEmbedded.java | 3 +- .../service/ai/WebAIProfileUtilsTest.java | 77 +++++++++++++++++++ .../CBSecretControllerEmbeddedTest.java | 5 ++ 5 files changed, 134 insertions(+), 17 deletions(-) diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/user/WebUser.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/user/WebUser.java index 0b8d2eabb95..04af62d32b6 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/user/WebUser.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/user/WebUser.java @@ -56,6 +56,10 @@ public boolean getEnabled() { return user.isEnabled(); } + public boolean isSecretStorage() { + return user.isSecretStorage(); + } + public void setEnabled(boolean enabled) { user.enableUser(enabled); } diff --git a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIProfileUtils.java b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIProfileUtils.java index ef587064e8d..9e64e2053d7 100644 --- a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIProfileUtils.java +++ b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIProfileUtils.java @@ -21,6 +21,7 @@ import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.ai.AIConfigurationProfile; +import org.jkiss.dbeaver.model.ai.AISettings; import org.jkiss.dbeaver.model.ai.engine.AIEngineProperties; import org.jkiss.dbeaver.model.ai.registry.AISettingsManager; import org.jkiss.dbeaver.model.auth.AuthProperty; @@ -54,7 +55,7 @@ public static boolean areCredentialsSaved( } DBSSecretController secretController = webSession.getUserContext().getSecretController(); Set credentialProperties = getCredentialPropertyIds(profile.getConfiguration()); - Map storedCredentials = isPersistentStorageAvailable(secretController) + Map storedCredentials = isPersistentStorageAvailable(webSession, secretController) ? getStoredCredentials(secretController, profile, credentialProperties) : getSessionCredentials(webSession, profile, false); return !storedCredentials.isEmpty(); @@ -68,12 +69,15 @@ public static void saveCredentials( validateUserProfile(webSession, profile); DBSSecretController secretController = webSession.getUserContext().getSecretController(); Set credentialProperties = getCredentialPropertyIds(profile.getConfiguration()); - if (!isPersistentStorageAvailable(secretController)) { + validateCredentialProperties(credentialProperties, credentials.keySet()); + if (!isPersistentStorageAvailable(webSession, secretController)) { + if (isPersistentStorageSupported(secretController)) { + clearPersistentCredentials(secretController, profile, credentialProperties); + } Map sessionCredentials = getSessionCredentials(webSession, profile, true); updateCredentials(sessionCredentials, credentialProperties, credentials); return; } - validateCredentialProperties(credentialProperties, credentials.keySet()); for (Map.Entry credential : credentials.entrySet()) { String value = credential.getValue() == null ? null : credential.getValue().toString(); String secretId = getSecretId(profile, credential.getKey()); @@ -86,6 +90,7 @@ public static void saveCredentials( ); } } + webSession.removeAttribute(getSessionCredentialsAttribute(profile)); } @NotNull @@ -93,10 +98,18 @@ public static AIConfigurationProfile getEffectiveProfile( @NotNull WebSession webSession, @NotNull AIConfigurationProfile profile ) throws DBException { - AIConfigurationProfile source = AISettingsManager.getStaticSettings() - .getConfigurationOrNull(profile.getProfileId()); + return getEffectiveProfile(webSession, profile, AISettingsManager.getStaticSettings()); + } + + @NotNull + static AIConfigurationProfile getEffectiveProfile( + @NotNull WebSession webSession, + @NotNull AIConfigurationProfile profile, + @NotNull AISettings settings + ) throws DBException { + AIConfigurationProfile source = settings.getConfigurationOrNull(profile.getProfileId()); if (source == null) { - source = AISettingsManager.getStaticSettings().getDefaultConfiguration(); + source = settings.getDefaultConfiguration(); } if (source.isGlobal()) { return source; @@ -104,7 +117,7 @@ public static AIConfigurationProfile getEffectiveProfile( validateUserProfile(webSession, source); DBSSecretController secretController = webSession.getUserContext().getSecretController(); - Map credentials = isPersistentStorageAvailable(secretController) + Map credentials = isPersistentStorageAvailable(webSession, secretController) ? getStoredCredentials(secretController, source, getCredentialPropertyIds(source.getConfiguration())) : getSessionCredentials(webSession, source, false); if (credentials.isEmpty()) { @@ -154,10 +167,9 @@ public static void deleteCredentials( @NotNull AIConfigurationProfile profile ) throws DBException { DBSSecretController secretController = webSession.getUserContext().getSecretController(); - if (isPersistentStorageAvailable(secretController)) { - secretController.deleteObjectSecrets(getSecretObject(profile)); - } else { - webSession.removeAttribute(getSessionCredentialsAttribute(profile)); + webSession.removeAttribute(getSessionCredentialsAttribute(profile)); + if (isPersistentStorageSupported(secretController)) { + clearPersistentCredentials(secretController, profile, getCredentialPropertyIds(profile.getConfiguration())); } } @@ -205,12 +217,33 @@ private static void validateCredentialProperties( } } - private static boolean isPersistentStorageAvailable(@NotNull DBSSecretController secretController) throws DBException { + private static boolean isPersistentStorageAvailable( + @NotNull WebSession webSession, + @NotNull DBSSecretController secretController + ) throws DBException { + if (!isPersistentStorageSupported(secretController)) { + return false; + } + var user = webSession.getUserContext().getUser(); + return user != null && user.isSecretStorage(); + } + + private static boolean isPersistentStorageSupported(@NotNull DBSSecretController secretController) throws DBException { long features = secretController.getSupportedFeatures(); return (features & DBSSecretController.FEATURE_PRIVATE_SECRETS_VIEW) != 0 && (features & DBSSecretController.FEATURE_PRIVATE_SECRETS_EDIT) != 0; } + private static void clearPersistentCredentials( + @NotNull DBSSecretController secretController, + @NotNull AIConfigurationProfile profile, + @NotNull Set credentialProperties + ) throws DBException { + for (String property : credentialProperties) { + secretController.setPrivateSecretValue(getSecretId(profile, property), null); + } + } + @NotNull private static Map getSessionCredentials( @NotNull WebSession webSession, @@ -220,7 +253,7 @@ private static Map getSessionCredentials( String attribute = getSessionCredentialsAttribute(profile); synchronized (webSession) { SessionCredentials sessionCredentials = webSession.getAttribute(attribute); - if (sessionCredentials != null && sessionCredentials.profile() == profile) { + if (sessionCredentials != null) { if (create) { return sessionCredentials.credentials(); } @@ -231,7 +264,7 @@ private static Map getSessionCredentials( if (!create) { return Map.of(); } - SessionCredentials newCredentials = new SessionCredentials(profile, new HashMap<>()); + SessionCredentials newCredentials = new SessionCredentials(new HashMap<>()); webSession.setAttribute(attribute, newCredentials); return newCredentials.credentials(); } @@ -355,7 +388,6 @@ public String getSecretObjectType() { } private record SessionCredentials( - @NotNull AIConfigurationProfile profile, @NotNull Map credentials ) { } diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBSecretControllerEmbedded.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBSecretControllerEmbedded.java index 0c3f6ead5d7..ead53f04dcf 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBSecretControllerEmbedded.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBSecretControllerEmbedded.java @@ -48,8 +48,7 @@ public class CBSecretControllerEmbedded implements DBSSecretControllerAuthorized @Override public long getSupportedFeatures() { - return DBSSecretController.FEATURE_PRIVATE_SECRETS_VIEW | - DBSSecretController.FEATURE_PRIVATE_SECRETS_EDIT; + return 0; } @Nullable diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/service/ai/WebAIProfileUtilsTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/service/ai/WebAIProfileUtilsTest.java index 25bb97fa593..21cdbac9d2d 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/service/ai/WebAIProfileUtilsTest.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/service/ai/WebAIProfileUtilsTest.java @@ -18,8 +18,11 @@ import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.model.session.WebUserContext; +import io.cloudbeaver.model.user.WebUser; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.ai.AIConfigurationProfile; +import org.jkiss.dbeaver.model.ai.AISettings; +import org.jkiss.dbeaver.model.ai.engine.openai.OpenAIConstants; import org.jkiss.dbeaver.model.ai.engine.openai.OpenAIProperties; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.secret.DBSSecretController; @@ -37,6 +40,7 @@ public class WebAIProfileUtilsTest { private final Map secrets = new HashMap<>(); private final Map sessionAttributes = new HashMap<>(); private DBSSecretController secretController; + private WebUser user; private WebSession webSession; private AIConfigurationProfile profile; private OpenAIProperties properties; @@ -71,6 +75,9 @@ public void setUp() throws DBException { WebUserContext userContext = Mockito.mock(WebUserContext.class); Mockito.when(userContext.getSecretController()).thenReturn(secretController); + user = Mockito.mock(WebUser.class); + Mockito.when(user.isSecretStorage()).thenReturn(true); + Mockito.when(userContext.getUser()).thenReturn(user); webSession = Mockito.mock(WebSession.class); Mockito.when(webSession.getUserId()).thenReturn("test-user"); @@ -142,4 +149,74 @@ public void storesCredentialsInSessionWithoutPrivateSecretStorage() throws DBExc WebAIProfileUtils.saveCredentials(webSession, profile, Map.of(credentialPropertyId, "")); Assertions.assertFalse(WebAIProfileUtils.areCredentialsSaved(webSession, profile)); } + + @Test + public void usesSessionCredentialsAcrossProfileInstances() throws DBException { + Mockito.when(secretController.getSupportedFeatures()).thenReturn(0L); + WebAIProfileUtils.saveCredentials(webSession, profile, Map.of(credentialPropertyId, "session-token")); + AIConfigurationProfile sameProfile = Mockito.mock(AIConfigurationProfile.class); + Mockito.when(sameProfile.getProfileId()).thenReturn(profile.getProfileId()); + Mockito.when(sameProfile.getConfiguration()).thenReturn(properties); + Mockito.when(sameProfile.isGlobal()).thenReturn(false); + + Assertions.assertTrue(WebAIProfileUtils.areCredentialsSaved(webSession, sameProfile)); + } + + @Test + public void fallsBackToSessionWhenSubjectSecretStorageIsDisabled() throws DBException { + Mockito.when(user.isSecretStorage()).thenReturn(false); + + WebAIProfileUtils.saveCredentials(webSession, profile, Map.of(credentialPropertyId, "session-token")); + + Assertions.assertTrue(WebAIProfileUtils.areCredentialsSaved(webSession, profile)); + Assertions.assertTrue(secrets.isEmpty()); + } + + @Test + public void createsEffectiveProfileWithoutMutatingSource() throws DBException { + AIConfigurationProfile source = new AIConfigurationProfile(); + source.setProfileId("test-effective-profile"); + source.setProfileName("Effective profile test"); + source.setEngineId(OpenAIConstants.OPENAI_ENGINE); + source.setGlobal(false); + OpenAIProperties sourceProperties = new OpenAIProperties(); + sourceProperties.setGlobal(false); + source.setConfiguration(sourceProperties); + AISettings settings = Mockito.mock(AISettings.class); + Mockito.when(settings.getConfigurationOrNull(source.getProfileId())).thenReturn(source); + + WebAIProfileUtils.saveCredentials(webSession, source, Map.of(credentialPropertyId, "effective-token")); + + AIConfigurationProfile effective = WebAIProfileUtils.getEffectiveProfile(webSession, source, settings); + + Assertions.assertNotSame(source, effective); + Assertions.assertNotSame(sourceProperties, effective.getConfiguration()); + Assertions.assertEquals("effective-token", ((OpenAIProperties) effective.getConfiguration()).getToken()); + Assertions.assertNull(sourceProperties.getToken()); + } + + @Test + public void clearsPersistentCredentialsWhenSwitchingToSessionStorage() throws DBException { + WebAIProfileUtils.saveCredentials(webSession, profile, Map.of(credentialPropertyId, "persistent-token")); + Mockito.when(user.isSecretStorage()).thenReturn(false); + + WebAIProfileUtils.saveCredentials(webSession, profile, Map.of(credentialPropertyId, "session-token")); + + Assertions.assertTrue(secrets.isEmpty()); + Assertions.assertTrue(WebAIProfileUtils.areCredentialsSaved(webSession, profile)); + } + + @Test + public void clearsSessionCredentialsWhenSwitchingToPersistentStorage() throws DBException { + Mockito.when(secretController.getSupportedFeatures()).thenReturn(0L); + WebAIProfileUtils.saveCredentials(webSession, profile, Map.of(credentialPropertyId, "session-token")); + Mockito.when(secretController.getSupportedFeatures()).thenReturn( + DBSSecretController.FEATURE_PRIVATE_SECRETS_VIEW | DBSSecretController.FEATURE_PRIVATE_SECRETS_EDIT + ); + + WebAIProfileUtils.saveCredentials(webSession, profile, Map.of(credentialPropertyId, "persistent-token")); + Mockito.when(secretController.getSupportedFeatures()).thenReturn(0L); + + Assertions.assertFalse(WebAIProfileUtils.areCredentialsSaved(webSession, profile)); + } } diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CBSecretControllerEmbeddedTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CBSecretControllerEmbeddedTest.java index 0cd493ad8c3..abe195e9e0e 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CBSecretControllerEmbeddedTest.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CBSecretControllerEmbeddedTest.java @@ -52,6 +52,11 @@ public String getSecretObjectType() { } }; + @Test + public void doesNotAdvertisePlaintextSecretStorage() throws DBException { + Assertions.assertEquals(0, createController(TEST_USER_ID).getSupportedFeatures()); + } + @Test public void testPrivateSecretsPersistBetweenControllers() throws DBException { String secretId = "ce_private_secret_test"; From 171fb67903d7584dc4f876350d3b0834da97864b Mon Sep 17 00:00:00 2001 From: Ainur Date: Fri, 11 Sep 2026 17:00:11 +0200 Subject: [PATCH 30/31] dbeaver/pro#9532 Preserve data grid API names --- webapp/common-react/@dbeaver/react-data-grid/src/DataGrid.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/webapp/common-react/@dbeaver/react-data-grid/src/DataGrid.tsx b/webapp/common-react/@dbeaver/react-data-grid/src/DataGrid.tsx index 804e59f91ae..5a5bd5c9218 100644 --- a/webapp/common-react/@dbeaver/react-data-grid/src/DataGrid.tsx +++ b/webapp/common-react/@dbeaver/react-data-grid/src/DataGrid.tsx @@ -38,11 +38,13 @@ export interface ICellPosition { colIdx: number; } +// eslint-disable-next-line @typescript-eslint/naming-convention -- Preserve the public API name. export interface DataGridCellKeyboardEvent extends React.KeyboardEvent { preventGridDefault: () => void; isGridDefaultPrevented: () => boolean; } +// eslint-disable-next-line @typescript-eslint/naming-convention -- Preserve the public API name. export interface DataGridProps extends IDataGridCellContext, IDataGridRowContext, IDataGridHeaderCellContext, React.PropsWithChildren { getRowHeight?: (rowIdx: number) => number; getRowId?: (rowIdx: number) => React.Key; @@ -61,6 +63,7 @@ export interface DataGridProps extends IDataGridCellContext, IDataGridRowContext }; } +// eslint-disable-next-line @typescript-eslint/naming-convention -- Preserve the public API name. export interface DataGridRef { selectCell: (position: ICellPosition, options?: { deferred?: boolean }) => boolean; scrollToCell: (position: Partial) => void; From bddde07097a958cc94118307f18d90f168aad68c Mon Sep 17 00:00:00 2001 From: Ainur Date: Fri, 11 Sep 2026 17:10:17 +0200 Subject: [PATCH 31/31] dbeaver/pro#9532 Fix AI profile utility tests --- .../src/io/cloudbeaver/service/ai/WebAIProfileUtils.java | 2 +- .../src/io/cloudbeaver/service/ai/WebAIProfileUtilsTest.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIProfileUtils.java b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIProfileUtils.java index 9e64e2053d7..8db2aabdbcf 100644 --- a/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIProfileUtils.java +++ b/server/bundles/io.cloudbeaver.service.ai/src/io/cloudbeaver/service/ai/WebAIProfileUtils.java @@ -102,7 +102,7 @@ public static AIConfigurationProfile getEffectiveProfile( } @NotNull - static AIConfigurationProfile getEffectiveProfile( + public static AIConfigurationProfile getEffectiveProfile( @NotNull WebSession webSession, @NotNull AIConfigurationProfile profile, @NotNull AISettings settings diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/service/ai/WebAIProfileUtilsTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/service/ai/WebAIProfileUtilsTest.java index 21cdbac9d2d..5709d9caf6c 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/service/ai/WebAIProfileUtilsTest.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/service/ai/WebAIProfileUtilsTest.java @@ -155,7 +155,8 @@ public void usesSessionCredentialsAcrossProfileInstances() throws DBException { Mockito.when(secretController.getSupportedFeatures()).thenReturn(0L); WebAIProfileUtils.saveCredentials(webSession, profile, Map.of(credentialPropertyId, "session-token")); AIConfigurationProfile sameProfile = Mockito.mock(AIConfigurationProfile.class); - Mockito.when(sameProfile.getProfileId()).thenReturn(profile.getProfileId()); + String profileId = profile.getProfileId(); + Mockito.when(sameProfile.getProfileId()).thenReturn(profileId); Mockito.when(sameProfile.getConfiguration()).thenReturn(properties); Mockito.when(sameProfile.isGlobal()).thenReturn(false);