diff --git a/build.gradle b/build.gradle index d05c50e6..72543108 100644 --- a/build.gradle +++ b/build.gradle @@ -98,8 +98,7 @@ intellij { version = "${ProductVersion}" downloadSources = false instrumentCode = false - // TODO: add plugins - // plugins = ["uml", "com.jetbrains.ChooseRuntime:1.0.9"] + plugins = ["com.intellij.database"] patchPluginXml { changeNotes = changelog.get(PluginVersion).toHTML() diff --git a/gradle.properties b/gradle.properties index 8d5cbb3a..69d4c81c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,12 +15,12 @@ PublishChannel=default # Possible values: # Release: 2021.2.0 # EAP: 2022.2.0-eap04 -RiderSdkVersion=2023.1.0-eap04 +RiderSdkVersion=2023.1.0-eap08 # Possible values (minor is omitted): # Release: 2020.2 # Nightly: 2020.3-SNAPSHOT # EAP: 2020.3-EAP2-SNAPSHOT -ProductVersion=2023.1-EAP4-SNAPSHOT +ProductVersion=2023.1-EAP8-SNAPSHOT # Kotlin 1.4 will bundle the stdlib dependency by default, causing problems with the version bundled with the IDE # https://blog.jetbrains.com/kotlin/2020/07/kotlin-1-4-rc-released/#stdlib-default diff --git a/src/dotnet/Plugin.props b/src/dotnet/Plugin.props index e4fdfe93..bae323ba 100644 --- a/src/dotnet/Plugin.props +++ b/src/dotnet/Plugin.props @@ -1,13 +1,13 @@  - 2023.1.0-eap01 + 2023.1.0-eap08 Entity Framework Core JetBrains Rider plugin for Entity Framework Core Andrew Rublyov - Copyright $([System.DateTime]::Now.Year) Andrew Rublyov + Copyright $([System.DateTime]::Now.Year) Andrii Rublov resharper plugin diff --git a/src/rider/main/kotlin/me/seclerp/observables/ui/dsl/ObservableDslEx.kt b/src/rider/main/kotlin/me/seclerp/observables/ui/dsl/ObservableDslEx.kt index f2a85271..3d4ae771 100644 --- a/src/rider/main/kotlin/me/seclerp/observables/ui/dsl/ObservableDslEx.kt +++ b/src/rider/main/kotlin/me/seclerp/observables/ui/dsl/ObservableDslEx.kt @@ -2,6 +2,7 @@ package me.seclerp.observables.ui.dsl import com.intellij.openapi.editor.event.DocumentEvent import com.intellij.openapi.editor.event.DocumentListener +import com.intellij.openapi.observable.util.whenTextChanged import com.intellij.openapi.project.Project import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.TextFieldWithBrowseButton @@ -15,8 +16,11 @@ import me.seclerp.observables.ObservableProperty import me.seclerp.rider.plugins.efcore.ui.IconComboBoxRendererAdapter import me.seclerp.rider.plugins.efcore.ui.items.IconItem import java.awt.event.ItemEvent +import javax.swing.ComboBoxEditor import javax.swing.DefaultComboBoxModel import javax.swing.Icon +import javax.swing.JTextField +import javax.swing.plaf.basic.BasicComboBoxEditor fun > Row.iconComboBox( selectedItemProperty: Observable, @@ -49,6 +53,49 @@ fun > Row.iconComboBox( } } +fun , TValue> Row.editableComboBox( + selectedTextProperty: Observable, + availableItemsProperty: Observable>, + itemMapper: (TValue) -> String +): Cell> { + val model = DefaultComboBoxModel() + .apply { + availableItemsProperty.afterChange { + removeAllElements() + addAll(it) + } + } + + return comboBox(model, IconComboBoxRendererAdapter()) + .applyToComponent { + isEditable = true + editor = object : BasicComboBoxEditor() { + override fun setItem(anObject: Any?) { + val item = anObject as? IconItem + if (item == null && anObject is String?) + editor.text = anObject + else if (item != null) + editor.text = itemMapper(item.data) + } + + override fun getItem(): Any { + return editor.text + } + } + + val editorComponent = editor.editorComponent as JTextField + + editorComponent.whenTextChanged { + selectedTextProperty.value = editorComponent.text + } + + selectedTextProperty.afterChange { + editorComponent.text = it + } + } + .align(AlignX.FILL) +} + fun Row.textFieldWithCompletion( property: ObservableProperty, completions: MutableList, diff --git a/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/connections/AppSettingsConnectionProvider.kt b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/connections/AppSettingsConnectionProvider.kt new file mode 100644 index 00000000..29511c8f --- /dev/null +++ b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/connections/AppSettingsConnectionProvider.kt @@ -0,0 +1,41 @@ +package me.seclerp.rider.plugins.efcore.features.connections + +import com.fasterxml.jackson.databind.node.ObjectNode +import com.fasterxml.jackson.databind.node.TextNode +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.util.io.isFile +import com.jetbrains.rider.model.RdCustomLocation +import com.jetbrains.rider.model.RdProjectDescriptor +import kotlin.io.path.Path +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.name + +@Service +class AppSettingsConnectionProvider : DbConnectionProvider { + companion object { + private val json = jacksonObjectMapper() + fun getInstance(intellijProject: Project) = intellijProject.service() + } + + override fun getAvailableConnections(project: RdProjectDescriptor) = + buildList { + val directory = (project.location as RdCustomLocation?)?.customLocation?.let(::Path)?.parent ?: return@buildList + val connectionStrings = directory.listDirectoryEntries("appsettings*.json") + .filter { it.isFile() } + .map { it.name to json.readTree(it.toFile()) } + .mapNotNull { (fileName, json) -> (json.get("ConnectionStrings") as ObjectNode?)?.let { fileName to it } } + .flatMap { (fileName, obj) -> + obj.fieldNames().asSequence().map { connName -> + (obj[connName] as TextNode?)?.let { node -> Triple(fileName, connName, node.textValue()) } + } + } + .filterNotNull() + .map { (fileName, connName, connString) -> DbConnectionInfo(connName, connString, fileName, null) } + + addAll(connectionStrings) + }.toList() +} + diff --git a/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/connections/DataGripConnectionProvider.kt b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/connections/DataGripConnectionProvider.kt new file mode 100644 index 00000000..d5e59024 --- /dev/null +++ b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/connections/DataGripConnectionProvider.kt @@ -0,0 +1,29 @@ +package me.seclerp.rider.plugins.efcore.features.connections + +import com.intellij.database.Dbms +import com.intellij.database.dataSource.LocalDataSource +import com.intellij.database.dataSource.LocalDataSourceManager +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.jetbrains.rider.model.RdProjectDescriptor + +@Service +class DataGripConnectionProvider(private val intellijProject: Project) : DbConnectionProvider { + companion object { + fun getInstance(intellijProject: Project) = intellijProject.service() + } + override fun getAvailableConnections(project: RdProjectDescriptor) = buildList { + LocalDataSourceManager.getInstance(intellijProject).dataSources.forEach { + val connString = generateConnectionString(it) + if (connString != null) + add(DbConnectionInfo(it.name, connString, "Data sources", it.dbms)) + } + } + + private fun generateConnectionString(source: LocalDataSource) = + when (source.dbms) { + Dbms.SQLITE -> "Data Source=${source.url?.removePrefix("jdbc:sqlite:")}" + else -> null + } +} \ No newline at end of file diff --git a/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/connections/DbConnectionInfo.kt b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/connections/DbConnectionInfo.kt new file mode 100644 index 00000000..f9e53143 --- /dev/null +++ b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/connections/DbConnectionInfo.kt @@ -0,0 +1,11 @@ +package me.seclerp.rider.plugins.efcore.features.connections + +import com.intellij.database.Dbms + +data class DbConnectionInfo( + val name: String, + val connectionString: String, + val sourceName: String, + val dbms: Dbms? +) + diff --git a/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/connections/DbConnectionProvider.kt b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/connections/DbConnectionProvider.kt new file mode 100644 index 00000000..5d895835 --- /dev/null +++ b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/connections/DbConnectionProvider.kt @@ -0,0 +1,7 @@ +package me.seclerp.rider.plugins.efcore.features.connections + +import com.jetbrains.rider.model.RdProjectDescriptor + +interface DbConnectionProvider { + fun getAvailableConnections(project: RdProjectDescriptor): List +} \ No newline at end of file diff --git a/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/connections/DbConnectionsCollector.kt b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/connections/DbConnectionsCollector.kt new file mode 100644 index 00000000..822405b9 --- /dev/null +++ b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/connections/DbConnectionsCollector.kt @@ -0,0 +1,34 @@ +package me.seclerp.rider.plugins.efcore.features.connections + +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.workspaceModel.ide.WorkspaceModel +import com.jetbrains.rider.model.RdProjectDescriptor +import com.jetbrains.rider.projectView.workspace.findProjects +import java.util.* + +@Service +@Suppress("UnstableApiUsage") +class DbConnectionsCollector(private val intellijProject: Project) { + companion object { + fun getInstance(intellijProject: Project) = intellijProject.service() + } + + private val providers = listOf( + AppSettingsConnectionProvider.getInstance(intellijProject), + UserSecretsConnectionProvider.getInstance(intellijProject), + DataGripConnectionProvider.getInstance(intellijProject) + ) + + fun collect(projectId: UUID): List { + val project = WorkspaceModel.getInstance(intellijProject) + .findProjects() + .filter { it.descriptor is RdProjectDescriptor } + .map { it.descriptor as RdProjectDescriptor } + .firstOrNull { it.originalGuid == projectId } + ?: return emptyList() + + return providers.flatMap { it.getAvailableConnections(project) } + } +} \ No newline at end of file diff --git a/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/connections/UserSecretsConnectionProvider.kt b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/connections/UserSecretsConnectionProvider.kt new file mode 100644 index 00000000..37a0e5d9 --- /dev/null +++ b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/connections/UserSecretsConnectionProvider.kt @@ -0,0 +1,39 @@ +package me.seclerp.rider.plugins.efcore.features.connections + +import com.fasterxml.jackson.databind.node.ObjectNode +import com.fasterxml.jackson.databind.node.TextNode +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.SystemInfo +import com.jetbrains.rider.ideaInterop.welcomeWizard.transferSettingsRider.utilities.WindowsEnvVariables +import com.jetbrains.rider.model.RdProjectDescriptor +import com.jetbrains.rider.projectView.nodes.getUserData +import kotlin.io.path.Path + +@Service +class UserSecretsConnectionProvider : DbConnectionProvider { + companion object { + private val json = jacksonObjectMapper() + private val userSecretsFolder = if (SystemInfo.isWindows) + Path(WindowsEnvVariables.applicationData, "Microsoft", "UserSecrets") + else + Path(System.getenv("HOME"), ".microsoft", "usersecrets") + fun getInstance(intellijProject: Project) = intellijProject.service() + } + + override fun getAvailableConnections(project: RdProjectDescriptor) = + buildList { + val userSecretsId = project.getUserData("UserSecretsId") ?: return@buildList + val userSecretsFile = userSecretsFolder.resolve(userSecretsId).resolve("secrets.json").toFile() + if (!userSecretsFile.exists() || !userSecretsFile.isFile) + return@buildList + val obj = json.readTree(userSecretsFile).get("ConnectionStrings") as ObjectNode? ?: return@buildList + obj.fieldNames().forEach { connName -> + val connString = (obj[connName] as TextNode?)?.textValue() + if (connString != null) + add(DbConnectionInfo(connName, connString, "User secrets", null)) + } + }.toList() +} \ No newline at end of file diff --git a/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/database/update/UpdateDatabaseDataContext.kt b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/database/update/UpdateDatabaseDataContext.kt index daa99801..c5734804 100644 --- a/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/database/update/UpdateDatabaseDataContext.kt +++ b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/database/update/UpdateDatabaseDataContext.kt @@ -4,6 +4,7 @@ import com.intellij.openapi.project.Project import me.seclerp.observables.bind import me.seclerp.observables.observable import me.seclerp.observables.observableList +import me.seclerp.rider.plugins.efcore.features.shared.ObservableConnections import me.seclerp.rider.plugins.efcore.features.shared.ObservableMigrations import me.seclerp.rider.plugins.efcore.features.shared.dialog.CommonDataContext import me.seclerp.rider.plugins.efcore.state.DialogsStateService @@ -11,6 +12,7 @@ import me.seclerp.rider.plugins.efcore.state.DialogsStateService class UpdateDatabaseDataContext(intellijProject: Project): CommonDataContext(intellijProject, true) { val observableMigrations = ObservableMigrations(intellijProject, migrationsProject, dbContext) val availableMigrationNames = observableList() + val observableConnections = ObservableConnections(intellijProject, startupProject) val migrationNames = observableList() .apply { @@ -29,6 +31,7 @@ class UpdateDatabaseDataContext(intellijProject: Project): CommonDataContext(int super.initBindings() observableMigrations.initBinding() + observableConnections.initBinding() availableMigrationNames.bind(migrationNames) { buildList { diff --git a/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/database/update/UpdateDatabaseDialogWrapper.kt b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/database/update/UpdateDatabaseDialogWrapper.kt index ee0d9c85..2c4794ee 100644 --- a/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/database/update/UpdateDatabaseDialogWrapper.kt +++ b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/database/update/UpdateDatabaseDialogWrapper.kt @@ -11,11 +11,14 @@ import me.seclerp.observables.bind import me.seclerp.observables.observable import me.seclerp.observables.observableList import me.seclerp.observables.ui.dsl.bindSelected -import me.seclerp.observables.ui.dsl.bindText +import me.seclerp.observables.ui.dsl.editableComboBox import me.seclerp.observables.ui.dsl.iconComboBox import me.seclerp.rider.plugins.efcore.cli.api.DatabaseCommandFactory import me.seclerp.rider.plugins.efcore.cli.api.models.DotnetEfVersion +import me.seclerp.rider.plugins.efcore.features.connections.DbConnectionInfo import me.seclerp.rider.plugins.efcore.features.shared.dialog.CommonDialogWrapper +import me.seclerp.rider.plugins.efcore.ui.DbConnectionItemRenderer +import me.seclerp.rider.plugins.efcore.ui.items.DbConnectionItem import me.seclerp.rider.plugins.efcore.ui.items.MigrationItem import java.util.* @@ -37,8 +40,8 @@ class UpdateDatabaseDialogWrapper( // // Internal data private val targetMigrationsView = observableList() - private val targetMigrationView = observable(null) + private val availableDbConnectionsView = observableList() // // Validation @@ -62,6 +65,10 @@ class UpdateDatabaseDialogWrapper( targetMigrationView.bind(dataCtx.targetMigration, mappings.migration.toItem, mappings.migration.fromItem) + + availableDbConnectionsView.bind(dataCtx.observableConnections) { + it.map(mappings.dbConnection.toItem) + } } override fun generateCommand(): GeneralCommandLine { @@ -96,8 +103,8 @@ class UpdateDatabaseDialogWrapper( .component } row("Connection:") { - textField() - .bindText(dataCtx.connection) + editableComboBox(dataCtx.connection, availableDbConnectionsView) { it.connectionString } + .applyToComponent { renderer = DbConnectionItemRenderer() } .validationOnInput(validator.connectionValidation()) .validationOnApply(validator.connectionValidation()) .enabledIf(useDefaultConnectionCheckbox!!.selected.not()) @@ -117,6 +124,16 @@ class UpdateDatabaseDialogWrapper( val fromItem: (MigrationItem?) -> String? get() = { it?.data } } + + object dbConnection { + val toItem: (DbConnectionInfo) -> DbConnectionItem + get() = { + DbConnectionItem(it) + } + + val fromItem: (DbConnectionItem) -> DbConnectionInfo + get() = { it.data } + } } } } \ No newline at end of file diff --git a/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/database/update/UpdateDatabaseValidator.kt b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/database/update/UpdateDatabaseValidator.kt index 7f0eb649..61b809f0 100644 --- a/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/database/update/UpdateDatabaseValidator.kt +++ b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/database/update/UpdateDatabaseValidator.kt @@ -5,7 +5,9 @@ import com.intellij.openapi.ui.ValidationInfo import com.intellij.ui.components.JBTextField import com.intellij.ui.layout.ValidationInfoBuilder import com.intellij.util.textCompletion.TextFieldWithCompletion +import me.seclerp.rider.plugins.efcore.ui.items.DbConnectionItem import me.seclerp.rider.plugins.efcore.ui.items.MigrationItem +import javax.swing.text.JTextComponent class UpdateDatabaseValidator( private val currentDbContextMigrationsList: MutableList @@ -17,8 +19,8 @@ class UpdateDatabaseValidator( null } - fun connectionValidation(): ValidationInfoBuilder.(JBTextField) -> ValidationInfo? = { - if (it.isEnabled && it.text.isEmpty()) + fun connectionValidation(): ValidationInfoBuilder.(ComboBox) -> ValidationInfo? = { + if (it.isEnabled && (it.editor.editorComponent as JTextComponent).text.isEmpty()) error("Connection could not be empty") else null } diff --git a/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/dbcontext/scaffold/ScaffoldDbContextDataContext.kt b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/dbcontext/scaffold/ScaffoldDbContextDataContext.kt index d2f59564..50245d80 100644 --- a/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/dbcontext/scaffold/ScaffoldDbContextDataContext.kt +++ b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/dbcontext/scaffold/ScaffoldDbContextDataContext.kt @@ -3,12 +3,14 @@ package me.seclerp.rider.plugins.efcore.features.dbcontext.scaffold import com.intellij.openapi.project.Project import me.seclerp.observables.observable import me.seclerp.observables.observableList +import me.seclerp.rider.plugins.efcore.features.shared.ObservableConnections import me.seclerp.rider.plugins.efcore.features.shared.dialog.CommonDataContext import me.seclerp.rider.plugins.efcore.state.DialogsStateService import me.seclerp.rider.plugins.efcore.ui.items.SimpleItem class ScaffoldDbContextDataContext(intellijProject: Project) : CommonDataContext(intellijProject, false) { val connection = observable("") + val observableConnections = ObservableConnections(intellijProject, startupProject) val provider = observable("") val outputFolder = observable("Entities") @@ -26,6 +28,12 @@ class ScaffoldDbContextDataContext(intellijProject: Project) : CommonDataContext val scaffoldAllTables = observable(true) val scaffoldAllSchemas = observable(true) + override fun initBindings() { + super.initBindings() + + observableConnections.initBinding() + } + override fun loadState(commonDialogState: DialogsStateService.SpecificDialogState) { super.loadState(commonDialogState) diff --git a/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/dbcontext/scaffold/ScaffoldDbContextDialogWrapper.kt b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/dbcontext/scaffold/ScaffoldDbContextDialogWrapper.kt index c7713a3a..178e89ce 100644 --- a/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/dbcontext/scaffold/ScaffoldDbContextDialogWrapper.kt +++ b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/dbcontext/scaffold/ScaffoldDbContextDialogWrapper.kt @@ -15,6 +15,7 @@ import com.intellij.ui.table.JBTable import me.seclerp.observables.ObservableProperty import me.seclerp.observables.bind import me.seclerp.observables.observable +import me.seclerp.observables.observableList import me.seclerp.rider.plugins.efcore.cli.api.DbContextCommandFactory import me.seclerp.rider.plugins.efcore.ui.items.SimpleItem import me.seclerp.rider.plugins.efcore.ui.items.SimpleListTableModel @@ -22,6 +23,11 @@ import me.seclerp.rider.plugins.efcore.cli.api.models.DotnetEfVersion import me.seclerp.rider.plugins.efcore.features.shared.dialog.CommonDialogWrapper import me.seclerp.observables.ui.dsl.bindSelected import me.seclerp.observables.ui.dsl.bindText +import me.seclerp.observables.ui.dsl.editableComboBox +import me.seclerp.rider.plugins.efcore.features.connections.DbConnectionInfo +import me.seclerp.rider.plugins.efcore.ui.DbConnectionItemRenderer +import me.seclerp.rider.plugins.efcore.ui.items.DbConnectionItem +import me.seclerp.rider.plugins.efcore.ui.items.MigrationItem import me.seclerp.rider.plugins.efcore.ui.textFieldForRelativeFolder import java.io.File import java.util.* @@ -53,6 +59,7 @@ class ScaffoldDbContextDialogWrapper( private val schemasModel = SimpleListTableModel(dataCtx.schemasList) private val migrationProjectFolder = observable("") + private val availableDbConnectionsView = observableList() // // Validation @@ -73,6 +80,10 @@ class ScaffoldDbContextDialogWrapper( else "" } + + availableDbConnectionsView.bind(dataCtx.observableConnections) { + it.map(mappings.dbConnection.toItem) + } } override fun generateCommand(): GeneralCommandLine { @@ -128,8 +139,8 @@ class ScaffoldDbContextDialogWrapper( override fun Panel.createPrimaryOptions() { row("Connection:") { - textField() - .bindText(dataCtx.connection) + editableComboBox(dataCtx.connection, availableDbConnectionsView) { it.connectionString } + .applyToComponent { renderer = DbConnectionItemRenderer() } .align(AlignX.FILL) .validationOnInput(validator.connectionValidation()) .validationOnApply(validator.connectionValidation()) @@ -252,4 +263,18 @@ class ScaffoldDbContextDialogWrapper( }.resizableRow() } } + + companion object { + private object mappings { + object dbConnection { + val toItem: (DbConnectionInfo) -> DbConnectionItem + get() = { + DbConnectionItem(it) + } + + val fromItem: (DbConnectionItem) -> DbConnectionInfo + get() = { it.data } + } + } + } } \ No newline at end of file diff --git a/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/dbcontext/scaffold/ScaffoldDbContextValidator.kt b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/dbcontext/scaffold/ScaffoldDbContextValidator.kt index 3743ebe6..eae40b86 100644 --- a/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/dbcontext/scaffold/ScaffoldDbContextValidator.kt +++ b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/dbcontext/scaffold/ScaffoldDbContextValidator.kt @@ -1,16 +1,18 @@ package me.seclerp.rider.plugins.efcore.features.dbcontext.scaffold +import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.TextFieldWithBrowseButton import com.intellij.openapi.ui.ValidationInfo import com.intellij.ui.layout.ValidationInfoBuilder +import me.seclerp.rider.plugins.efcore.ui.items.DbConnectionItem import javax.swing.JTextField +import javax.swing.text.JTextComponent class ScaffoldDbContextValidator { - fun connectionValidation(): ValidationInfoBuilder.(JTextField) -> ValidationInfo? = { - if (it.text.trim().isEmpty()) + fun connectionValidation(): ValidationInfoBuilder.(ComboBox) -> ValidationInfo? = { + if (it.isEnabled && (it.editor.editorComponent as JTextComponent).text.isEmpty()) error("Connection could not be empty") - else - null + else null } fun providerValidation(): ValidationInfoBuilder.(JTextField) -> ValidationInfo? = { diff --git a/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/shared/ObservableConnections.kt b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/shared/ObservableConnections.kt new file mode 100644 index 00000000..db1c0929 --- /dev/null +++ b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/features/shared/ObservableConnections.kt @@ -0,0 +1,25 @@ +package me.seclerp.rider.plugins.efcore.features.shared + +import com.intellij.openapi.project.Project +import me.seclerp.observables.ObservableCollection +import me.seclerp.observables.ObservableProperty +import me.seclerp.observables.bind +import me.seclerp.rider.plugins.efcore.features.connections.DbConnectionInfo +import me.seclerp.rider.plugins.efcore.features.connections.DbConnectionsCollector +import me.seclerp.rider.plugins.efcore.rd.StartupProjectInfo + +class ObservableConnections( + private val intellijProject: Project, + private val startupProject: ObservableProperty +): ObservableCollection() { + private val connectionsCollector by lazy { DbConnectionsCollector.getInstance(intellijProject) } + fun initBinding() { + this.bind(startupProject) { + if (it != null) { + connectionsCollector.collect(it.id) + } else { + listOf() + } + } + } +} \ No newline at end of file diff --git a/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/ui/DbConnectionItemRenderer.kt b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/ui/DbConnectionItemRenderer.kt new file mode 100644 index 00000000..03a65a47 --- /dev/null +++ b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/ui/DbConnectionItemRenderer.kt @@ -0,0 +1,77 @@ +package me.seclerp.rider.plugins.efcore.ui + +import com.intellij.execution.runToolbar.components.TrimmedMiddleLabel +import com.intellij.ui.IdeBorderFactory +import com.intellij.ui.JBColor +import com.intellij.util.ui.JBInsets +import com.intellij.util.ui.UIUtil +import me.seclerp.rider.plugins.efcore.ui.items.DbConnectionItem +import java.awt.Component +import java.awt.GridBagConstraints +import java.awt.GridBagLayout +import javax.swing.JList +import javax.swing.JPanel +import javax.swing.ListCellRenderer + +class DbConnectionItemRenderer : ListCellRenderer { + private val connectionNameComponent = createColumnComponent().apply { foreground = JBColor.BLACK } + private val connectionStringComponent = createColumnComponent().apply { foreground = JBColor.GRAY } + private val sourceNameComponent = createColumnComponent().apply { foreground = JBColor.GRAY } + + private val rowComponent = createRowComponent().apply { + add(connectionNameComponent, GridBagConstraints( + 0, 0, + 1, 1, + 0.0, 0.0, + GridBagConstraints.BASELINE, + GridBagConstraints.NONE, + JBInsets.emptyInsets(), + 0, 0 + )) + add(connectionStringComponent, GridBagConstraints( + 1, 0, + 1, 1, + 1.0, 0.0, + GridBagConstraints.BASELINE, + GridBagConstraints.HORIZONTAL, + JBInsets.create(0, 10), + 0, 0 + )) + add(sourceNameComponent, GridBagConstraints( + 2, 0, + 1, 1, + 0.0, 0.0, + GridBagConstraints.BASELINE, + GridBagConstraints.NONE, + JBInsets.emptyInsets(), + 0, 0 + )) + } + + override fun getListCellRendererComponent(list: JList?, value: DbConnectionItem?, index: Int, isSelected: Boolean, cellHasFocus: Boolean): Component { + rowComponent.apply { + background = if (isSelected) list?.selectionBackground else list?.background + if (isEnabled != list?.isEnabled) { + UIUtil.setEnabled(this, list?.isEnabled ?: false, true) + } + } + + connectionNameComponent.icon = value?.icon + connectionNameComponent.text = value?.data?.name + connectionStringComponent.text = value?.data?.connectionString + sourceNameComponent.text = value?.data?.sourceName + + return rowComponent + } + + private fun createRowComponent() = + JPanel(GridBagLayout()).apply { + border = IdeBorderFactory.createEmptyBorder(insets) + } + + private fun createColumnComponent() = + TrimmedMiddleLabel().apply { + isOpaque = false + border = null + } +} \ No newline at end of file diff --git a/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/ui/items/DbConnectionItem.kt b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/ui/items/DbConnectionItem.kt new file mode 100644 index 00000000..4530ef7e --- /dev/null +++ b/src/rider/main/kotlin/me/seclerp/rider/plugins/efcore/ui/items/DbConnectionItem.kt @@ -0,0 +1,7 @@ +package me.seclerp.rider.plugins.efcore.ui.items + +import icons.DatabaseIcons +import me.seclerp.rider.plugins.efcore.features.connections.DbConnectionInfo + +class DbConnectionItem(data: DbConnectionInfo) + : IconItem(data.name, data.dbms?.icon ?: DatabaseIcons.Dbms, data) \ No newline at end of file diff --git a/src/rider/main/resources/META-INF/plugin.xml b/src/rider/main/resources/META-INF/plugin.xml index d6533ff9..0494d31a 100644 --- a/src/rider/main/resources/META-INF/plugin.xml +++ b/src/rider/main/resources/META-INF/plugin.xml @@ -5,6 +5,7 @@ Andrii Rublov com.intellij.modules.rider + com.intellij.database