From 15f48e368722eba43922482b0805aae924862e61 Mon Sep 17 00:00:00 2001 From: bluelhf Date: Thu, 4 Aug 2022 02:31:14 +0300 Subject: [PATCH 1/5] feat: imporved many things --- .gitignore | 2 +- CHANGELOG.md | 7 - README.md | 44 +++--- build.gradle.kts | 6 +- gradle.properties | 14 +- ...tausTimeBundle.kt => TestaustimeBundle.kt} | 4 +- ...onfigurable.kt => ConfigurableSettings.kt} | 26 ++-- .../IntellijSettingsComponent.kt | 40 ----- .../configuration/SettingsComponent.kt | 111 ++++++++++++++ ...sTimeSettingsState.kt => SettingsState.kt} | 10 +- .../TestausTimeProjectManagerListener.kt | 17 --- .../listeners/TestaustimeServiceListener.kt | 27 ++++ .../network/TestausTimeApiClient.kt | 32 ---- .../network/TestaustimeAPIClient.kt | 69 +++++++++ .../services/TestausTimeApplicationService.kt | 12 -- .../services/TestausTimeProjectService.kt | 88 ----------- .../services/TestaustimeApplicationService.kt | 138 ++++++++++++++++++ .../services/TestaustimeProjectService.kt | 32 ++++ .../utils/TestausTimeNotifier.kt | 19 --- .../utils/TestaustimeNotifier.kt | 45 ++++++ src/main/resources/META-INF/plugin.xml | 22 +-- .../resources/messages/MyBundle.properties | 3 - .../messages/TestaustimeBundle.properties | 23 +++ .../plugin_intellij/MyPluginTest.kt | 31 ---- src/test/testData/rename/foo.xml | 3 - src/test/testData/rename/foo_after.xml | 3 - 26 files changed, 506 insertions(+), 322 deletions(-) delete mode 100644 CHANGELOG.md rename src/main/kotlin/fi/testaustime/plugin_intellij/{TestausTimeBundle.kt => TestaustimeBundle.kt} (83%) rename src/main/kotlin/fi/testaustime/plugin_intellij/configuration/{IntellijSettingConfigurable.kt => ConfigurableSettings.kt} (57%) delete mode 100644 src/main/kotlin/fi/testaustime/plugin_intellij/configuration/IntellijSettingsComponent.kt create mode 100644 src/main/kotlin/fi/testaustime/plugin_intellij/configuration/SettingsComponent.kt rename src/main/kotlin/fi/testaustime/plugin_intellij/configuration/{TestausTimeSettingsState.kt => SettingsState.kt} (72%) delete mode 100644 src/main/kotlin/fi/testaustime/plugin_intellij/listeners/TestausTimeProjectManagerListener.kt create mode 100644 src/main/kotlin/fi/testaustime/plugin_intellij/listeners/TestaustimeServiceListener.kt delete mode 100644 src/main/kotlin/fi/testaustime/plugin_intellij/network/TestausTimeApiClient.kt create mode 100644 src/main/kotlin/fi/testaustime/plugin_intellij/network/TestaustimeAPIClient.kt delete mode 100644 src/main/kotlin/fi/testaustime/plugin_intellij/services/TestausTimeApplicationService.kt delete mode 100644 src/main/kotlin/fi/testaustime/plugin_intellij/services/TestausTimeProjectService.kt create mode 100644 src/main/kotlin/fi/testaustime/plugin_intellij/services/TestaustimeApplicationService.kt create mode 100644 src/main/kotlin/fi/testaustime/plugin_intellij/services/TestaustimeProjectService.kt delete mode 100644 src/main/kotlin/fi/testaustime/plugin_intellij/utils/TestausTimeNotifier.kt create mode 100644 src/main/kotlin/fi/testaustime/plugin_intellij/utils/TestaustimeNotifier.kt delete mode 100644 src/main/resources/messages/MyBundle.properties create mode 100644 src/main/resources/messages/TestaustimeBundle.properties delete mode 100644 src/test/kotlin/fi/testaustime/plugin_intellij/MyPluginTest.kt delete mode 100644 src/test/testData/rename/foo.xml delete mode 100644 src/test/testData/rename/foo_after.xml diff --git a/.gitignore b/.gitignore index e2e5d94..f890af7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ .gradle -.idea +**/.idea .qodana build diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 5e6077c..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ - - -# testaustime-intellij Changelog - -## [Unreleased] -### Added -- Initial scaffold created from [IntelliJ Platform Plugin Template](https://github.com/JetBrains/intellij-platform-plugin-template) diff --git a/README.md b/README.md index 8724a9b..bc50073 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,29 @@ # Testaustime IntelliJ Plugin ![Build](https://github.com/developerfromjokela/testaustime-intellij/workflows/Build/badge.svg) -[![Version](https://img.shields.io/jetbrains/plugin/v/PLUGIN_ID.svg)](https://plugins.jetbrains.com/plugin/PLUGIN_ID) -[![Downloads](https://img.shields.io/jetbrains/plugin/d/PLUGIN_ID.svg)](https://plugins.jetbrains.com/plugin/PLUGIN_ID) - -## Template ToDo list -- [x] Create a new [IntelliJ Platform Plugin Template][template] project. -- [ ] Get familiar with the [template documentation][template]. -- [ ] Verify the [pluginGroup](/gradle.properties), [plugin ID](/src/main/resources/META-INF/plugin.xml) and [sources package](/src/main/kotlin). -- [ ] Review the [Legal Agreements](https://plugins.jetbrains.com/docs/marketplace/legal-agreements.html). -- [ ] [Publish a plugin manually](https://plugins.jetbrains.com/docs/intellij/publishing-plugin.html?from=IJPluginTemplate) for the first time. -- [ ] Set the Plugin ID in the above README badges. -- [ ] Set the [Deployment Token](https://plugins.jetbrains.com/docs/marketplace/plugin-upload.html). -- [ ] Click the Watch button on the top of the [IntelliJ Platform Plugin Template][template] to be notified about releases containing new features and fixes. +[![Version](https://img.shields.io/jetbrains/plugin/v/19408.svg)](https://plugins.jetbrains.com/plugin/19408) +[![Downloads](https://img.shields.io/jetbrains/plugin/d/19408.svg)](https://plugins.jetbrains.com/plugin/19408) +--- Testaustime is the ultimate tool for tracking time of your coding sessions. Show the world how dedicated you are to your projects, now available for IntelliJ IDEs! ## Installation +> **Note** +> Here from [testaustime.fi](https://testaustime.fi)? You can skip steps 2 and 5! -- Using IDE built-in plugin system: +1. Install the Testaustime IntelliJ Plugin + - Using the IDE's built-in plugin system + - Settings > Plugins > Marketplace > Search for "Testaustime" > Install Plugin - Settings/Preferences > Plugins > Marketplace > Search for "Testaustime" > - Install Plugin - -- Manually: - - Download the [latest release](https://github.com/developerfromjokela/testaustime-intellij/releases/latest) and install it manually using - Settings/Preferences > Plugins > ⚙️ > Install plugin from disk... - - ---- -Plugin based on the [IntelliJ Platform Plugin Template][template]. - -[template]: https://github.com/JetBrains/intellij-platform-plugin-template + - Manually: + 1. Download the [latest release](https://github.com/developerfromjokela/testaustime-intellij/releases/latest) and install it manually using + 2. Settings > Plugins > ⚙️ > Install plugin from disk... + +2. (Optional) Set your Testaustime API endpoint + - Settings > Tools > Testaustime Settings > Testaustime API base URL +3. Set your Testaustime API token + - Settings > Tools > Testaustime Settings > Testaustime Authentication token +4. Start coding! +5. View your stats on your favourite Testaustime front-end! \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 923ec33..e19f8d7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,9 +7,9 @@ plugins { // Java support id("java") // Kotlin support - id("org.jetbrains.kotlin.jvm") version "1.6.10" + id("org.jetbrains.kotlin.jvm") version "1.7.10" // Gradle IntelliJ Plugin - id("org.jetbrains.intellij") version "1.4.0" + id("org.jetbrains.intellij") version "1.7.0" // Gradle Changelog Plugin id("org.jetbrains.changelog") version "1.3.1" // Gradle Qodana Plugin @@ -20,8 +20,6 @@ group = properties("pluginGroup") version = properties("pluginVersion") dependencies { - implementation("com.squareup.okhttp3:okhttp:4.10.0") - implementation("com.squareup.okhttp3:logging-interceptor:4.9.0") implementation("com.google.code.gson:gson:2.9.0") } diff --git a/gradle.properties b/gradle.properties index 378142e..4c156df 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,27 +4,27 @@ pluginGroup = fi.testaustime.plugin_intellij pluginName = testaustime-intellij # SemVer format -> https://semver.org -pluginVersion = 0.1.2 +pluginVersion = 0.2.0 # See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html # for insight into build numbers and IntelliJ Platform versions. -pluginSinceBuild = 211 -pluginUntilBuild = 216.* +pluginSinceBuild = 212.4746.92 +pluginUntilBuild = 282.* # IntelliJ Platform Properties -> https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties platformType = IC -platformVersion = 2021.1.3 +platformVersion = 2022.2 # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 platformPlugins = org.jetbrains.kotlin -# Java language level used to compile sources and to generate the files for - Java 11 is required since 2020.3 -javaVersion = 11 +# Java language level used to compile sources and to generate the files for - Java 17 is required since 2022.2 +javaVersion = 17 # Gradle Releases -> https://github.com/gradle/gradle/releases -gradleVersion = 7.4 +gradleVersion = 7.5 # Opt-out flag for bundling Kotlin standard library. # See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details. diff --git a/src/main/kotlin/fi/testaustime/plugin_intellij/TestausTimeBundle.kt b/src/main/kotlin/fi/testaustime/plugin_intellij/TestaustimeBundle.kt similarity index 83% rename from src/main/kotlin/fi/testaustime/plugin_intellij/TestausTimeBundle.kt rename to src/main/kotlin/fi/testaustime/plugin_intellij/TestaustimeBundle.kt index af7a796..f2bf6cd 100644 --- a/src/main/kotlin/fi/testaustime/plugin_intellij/TestausTimeBundle.kt +++ b/src/main/kotlin/fi/testaustime/plugin_intellij/TestaustimeBundle.kt @@ -5,9 +5,9 @@ import org.jetbrains.annotations.NonNls import org.jetbrains.annotations.PropertyKey @NonNls -private const val BUNDLE = "messages.MyBundle" +private const val BUNDLE = "messages.TestaustimeBundle" -object TestausTimeBundle : DynamicBundle(BUNDLE) { +object TestaustimeBundle : DynamicBundle(BUNDLE) { @Suppress("SpreadOperator") @JvmStatic diff --git a/src/main/kotlin/fi/testaustime/plugin_intellij/configuration/IntellijSettingConfigurable.kt b/src/main/kotlin/fi/testaustime/plugin_intellij/configuration/ConfigurableSettings.kt similarity index 57% rename from src/main/kotlin/fi/testaustime/plugin_intellij/configuration/IntellijSettingConfigurable.kt rename to src/main/kotlin/fi/testaustime/plugin_intellij/configuration/ConfigurableSettings.kt index a2c09ef..f970185 100644 --- a/src/main/kotlin/fi/testaustime/plugin_intellij/configuration/IntellijSettingConfigurable.kt +++ b/src/main/kotlin/fi/testaustime/plugin_intellij/configuration/ConfigurableSettings.kt @@ -1,41 +1,43 @@ package fi.testaustime.plugin_intellij.configuration +import com.intellij.openapi.components.service import com.intellij.openapi.options.Configurable +import fi.testaustime.plugin_intellij.TestaustimeBundle +import fi.testaustime.plugin_intellij.services.TestaustimeApplicationService import org.jetbrains.annotations.Nullable import javax.swing.JComponent -class IntellijSettingConfigurable : Configurable { - private var settingsComponent: IntellijSettingsComponent? = null - - - fun preferredFocusedComponent(): JComponent? { - return settingsComponent?.preferredFocusedComponent; - } +class ConfigurableSettings : Configurable { + private var settingsComponent: SettingsComponent? = null @Nullable override fun createComponent(): JComponent? { - settingsComponent = IntellijSettingsComponent() + settingsComponent = SettingsComponent() return settingsComponent?.panel } override fun isModified(): Boolean { - val settings: TestausTimeSettingsState = TestausTimeSettingsState.instance + val settings: SettingsState = SettingsState.instance var modified: Boolean = !settingsComponent?.baseUrl.equals(settings.apiBaseUrl) modified = modified or (!settingsComponent?.authToken.equals(settings.authToken)) + modified = modified and (settingsComponent?.validate() ?: false) return modified } override fun apply() { - val settings: TestausTimeSettingsState = TestausTimeSettingsState.instance + val settings: SettingsState = SettingsState.instance settings.apiBaseUrl = settingsComponent?.baseUrl ?: "https://api.testaustime.fi" settings.authToken = settingsComponent?.authToken ?: "" + + // Validate token immediately + service().pingNow(); } override fun reset() { - val settings: TestausTimeSettingsState = TestausTimeSettingsState.instance + val settings: SettingsState = SettingsState.instance settingsComponent?.baseUrl = settings.apiBaseUrl settingsComponent?.authToken = settings.authToken } @@ -45,6 +47,6 @@ class IntellijSettingConfigurable : Configurable { } override fun getDisplayName(): String { - return "Testaustime Settings" + return TestaustimeBundle.message("name"); } } \ No newline at end of file diff --git a/src/main/kotlin/fi/testaustime/plugin_intellij/configuration/IntellijSettingsComponent.kt b/src/main/kotlin/fi/testaustime/plugin_intellij/configuration/IntellijSettingsComponent.kt deleted file mode 100644 index cb4ea08..0000000 --- a/src/main/kotlin/fi/testaustime/plugin_intellij/configuration/IntellijSettingsComponent.kt +++ /dev/null @@ -1,40 +0,0 @@ -package fi.testaustime.plugin_intellij.configuration - -import com.intellij.ui.components.JBLabel -import com.intellij.ui.components.JBTextField -import com.intellij.util.ui.FormBuilder -import org.jetbrains.annotations.NotNull -import javax.swing.JComponent -import javax.swing.JPanel - - -class IntellijSettingsComponent { - val panel: JPanel - private val baseUrlText = JBTextField() - private val authTokenText = JBTextField() - - init { - panel = FormBuilder.createFormBuilder() - .addLabeledComponent(JBLabel("Testaustime API Base URL: "), baseUrlText, 1, false) - .addLabeledComponent(JBLabel("Testaustime Authentication token: "), authTokenText, 1, false) - .addComponentFillVertically(JPanel(), 0) - .panel - } - - val preferredFocusedComponent: JComponent - get() = baseUrlText - - @get:NotNull - var baseUrl: String? - get() = baseUrlText.text - set(newText) { - baseUrlText.text = newText - } - - @get:NotNull - var authToken: String? - get() = authTokenText.text - set(newText) { - authTokenText.text = newText - } -} \ No newline at end of file diff --git a/src/main/kotlin/fi/testaustime/plugin_intellij/configuration/SettingsComponent.kt b/src/main/kotlin/fi/testaustime/plugin_intellij/configuration/SettingsComponent.kt new file mode 100644 index 0000000..8a8f142 --- /dev/null +++ b/src/main/kotlin/fi/testaustime/plugin_intellij/configuration/SettingsComponent.kt @@ -0,0 +1,111 @@ +package fi.testaustime.plugin_intellij.configuration + +import com.intellij.openapi.ui.ComponentValidator +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBTextField +import com.intellij.util.ui.FormBuilder +import fi.testaustime.plugin_intellij.TestaustimeBundle.message +import fi.testaustime.plugin_intellij.network.TestaustimeAPIClient +import org.jetbrains.annotations.NotNull +import java.net.MalformedURLException +import java.net.URL +import java.util.function.Supplier +import javax.swing.InputVerifier +import javax.swing.JComponent +import javax.swing.JPanel + + +class SettingsComponent { + fun validate(): Boolean { + var valid = baseUrlText.inputVerifier?.verify(baseUrlText) ?: false; + valid = valid and (authTokenText.inputVerifier?.verify(authTokenText) ?: false); + return valid + } + + val panel: JPanel + private val baseUrlText = JBTextField() + private val authTokenText = JBTextField() + + init { + panel = FormBuilder.createFormBuilder() + .addLabeledComponent(JBLabel(message("settings.apiBaseURL") + ": "), baseUrlText, 1, false) + .addLabeledComponent(JBLabel(message("settings.apiToken") + ": "), authTokenText, 1, false) + .addComponentFillVertically(JPanel(), 0) + .panel + + run { + val validator = ComponentValidator { }.withValidator(Supplier { + if (baseUrlText.text.endsWith("/")) { + return@Supplier ValidationInfo(message("settings.apiBaseURL.noSlash"), baseUrlText) + } else { + try { + URL(baseUrlText.text) + } catch (ex: MalformedURLException) { + return@Supplier ValidationInfo(message("settings.apiBaseURL.mustBeURL"), baseUrlText) + } + } + + return@Supplier ValidationInfo("").withOKEnabled(); + }).installOn(baseUrlText) + + baseUrlText.inputVerifier = object : InputVerifier() { + override fun verify(input: JComponent?): Boolean { + validator.revalidate() + return validator.validationInfo?.okEnabled ?: true; + } + } + } + + + run { + val validator = ComponentValidator { }.withValidator(Supplier { + if (authTokenText.text.isEmpty()) { + return@Supplier ValidationInfo(message("settings.apiToken.needToken"), authTokenText).asWarning(); + } + + if (!authTokenText.text.chars().allMatch(Character::isLetterOrDigit)) { + return@Supplier ValidationInfo(message("settings.apiToken.mustBeAlphanumeric"), authTokenText); + } + + if (authTokenText.text.length != 32) { + return@Supplier ValidationInfo(message("settings.apiToken.invalidLength", authTokenText.text.length), + authTokenText); + } + + if (!TestaustimeAPIClient.verifyToken(baseUrlText.text, authTokenText.text)) { + return@Supplier ValidationInfo(message("settings.apiToken.invalid", baseUrl ?: "The API"), authTokenText); + } + + return@Supplier ValidationInfo("").withOKEnabled(); + }).installOn(authTokenText) + + authTokenText.inputVerifier = object : InputVerifier() { + override fun verify(input: JComponent?): Boolean { + validator.revalidate() + validator.validationInfo?.let { + return it.okEnabled or it.warning; + } + return false; + } + } + } + } + + val preferredFocusedComponent: JComponent + get() = baseUrlText + + @get:NotNull + var baseUrl: String? + get() = baseUrlText.text + set(newText) { + baseUrlText.text = newText + } + + @get:NotNull + var authToken: String? + get() = authTokenText.text + set(newText) { + authTokenText.text = newText + } +} \ No newline at end of file diff --git a/src/main/kotlin/fi/testaustime/plugin_intellij/configuration/TestausTimeSettingsState.kt b/src/main/kotlin/fi/testaustime/plugin_intellij/configuration/SettingsState.kt similarity index 72% rename from src/main/kotlin/fi/testaustime/plugin_intellij/configuration/TestausTimeSettingsState.kt rename to src/main/kotlin/fi/testaustime/plugin_intellij/configuration/SettingsState.kt index f52226d..495b1f3 100644 --- a/src/main/kotlin/fi/testaustime/plugin_intellij/configuration/TestausTimeSettingsState.kt +++ b/src/main/kotlin/fi/testaustime/plugin_intellij/configuration/SettingsState.kt @@ -10,21 +10,21 @@ import org.jetbrains.annotations.Nullable @State(name = "fi.testaustime.plugin_intellij.configuration.TestausTimeSettingsState", storages = [Storage("TestausTimePlugin.xml")]) -class TestausTimeSettingsState : PersistentStateComponent { +class SettingsState : PersistentStateComponent { var apiBaseUrl = "https://api.testaustime.fi" var authToken = "" @Nullable - override fun getState(): TestausTimeSettingsState { + override fun getState(): SettingsState { return this } - override fun loadState(@NotNull state: TestausTimeSettingsState) { + override fun loadState(@NotNull state: SettingsState) { XmlSerializerUtil.copyBean(state, this) } companion object { - val instance: TestausTimeSettingsState - get() = ApplicationManager.getApplication().getService(TestausTimeSettingsState::class.java) + val instance: SettingsState + get() = ApplicationManager.getApplication().getService(SettingsState::class.java) } } \ No newline at end of file diff --git a/src/main/kotlin/fi/testaustime/plugin_intellij/listeners/TestausTimeProjectManagerListener.kt b/src/main/kotlin/fi/testaustime/plugin_intellij/listeners/TestausTimeProjectManagerListener.kt deleted file mode 100644 index 6da6346..0000000 --- a/src/main/kotlin/fi/testaustime/plugin_intellij/listeners/TestausTimeProjectManagerListener.kt +++ /dev/null @@ -1,17 +0,0 @@ -package fi.testaustime.plugin_intellij.listeners - -import com.intellij.openapi.components.service -import com.intellij.openapi.project.Project -import com.intellij.openapi.project.ProjectManagerListener -import fi.testaustime.plugin_intellij.services.TestausTimeProjectService - -internal class TestausTimeProjectManagerListener : ProjectManagerListener { - - override fun projectOpened(project: Project) { - project.service() - } - - override fun projectClosed(project: Project) { - project.getService(TestausTimeProjectService::class.java).terminateService() - } -} diff --git a/src/main/kotlin/fi/testaustime/plugin_intellij/listeners/TestaustimeServiceListener.kt b/src/main/kotlin/fi/testaustime/plugin_intellij/listeners/TestaustimeServiceListener.kt new file mode 100644 index 0000000..788ec8b --- /dev/null +++ b/src/main/kotlin/fi/testaustime/plugin_intellij/listeners/TestaustimeServiceListener.kt @@ -0,0 +1,27 @@ +package fi.testaustime.plugin_intellij.listeners; + +import com.intellij.ide.AppLifecycleListener +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ProjectManagerListener +import fi.testaustime.plugin_intellij.services.TestaustimeApplicationService +import fi.testaustime.plugin_intellij.services.TestaustimeProjectService + +internal class TestaustimeServiceListener : AppLifecycleListener, ProjectManagerListener { + + override fun appStarted() { + service(); + } + + override fun appClosing() { + service().terminateService(); + } + + override fun projectOpened(project: Project) { + project.service() + } + + override fun projectClosingBeforeSave(project: Project) { + project.getService(TestaustimeProjectService::class.java).terminate() + } +} diff --git a/src/main/kotlin/fi/testaustime/plugin_intellij/network/TestausTimeApiClient.kt b/src/main/kotlin/fi/testaustime/plugin_intellij/network/TestausTimeApiClient.kt deleted file mode 100644 index e4b8115..0000000 --- a/src/main/kotlin/fi/testaustime/plugin_intellij/network/TestausTimeApiClient.kt +++ /dev/null @@ -1,32 +0,0 @@ -package fi.testaustime.plugin_intellij.network - -import com.google.gson.Gson -import fi.testaustime.plugin_intellij.network.models.ActivityPostPayload -import okhttp3.* -import okhttp3.MediaType.Companion.toMediaTypeOrNull - -class TestausTimeApiClient(private val baseUrl: String, token: String) { - private val httpClient = getHttpClient(token) - - companion object HTTPClient { - fun getHttpClient(token: String): OkHttpClient { - return OkHttpClient.Builder().addInterceptor(Interceptor {chain: Interceptor.Chain -> - val request = chain.request() - val newBuilder = request.newBuilder(); - newBuilder.addHeader("User-Agent", "TestausTimeClient-IntelliJ") - .addHeader("Authorization", "Bearer $token") - .addHeader("Content-Type", "application/json"); - chain.proceed(newBuilder.build()) - }).build() - } - } - - - fun activityLog(payload: ActivityPostPayload): Call { - val request = Request.Builder().url("$baseUrl/activity/update").method("POST", RequestBody.Companion.create( - "application/json".toMediaTypeOrNull(), Gson().toJson(payload) - )) - return httpClient.newCall(request.build()) - } - -} \ No newline at end of file diff --git a/src/main/kotlin/fi/testaustime/plugin_intellij/network/TestaustimeAPIClient.kt b/src/main/kotlin/fi/testaustime/plugin_intellij/network/TestaustimeAPIClient.kt new file mode 100644 index 0000000..af20525 --- /dev/null +++ b/src/main/kotlin/fi/testaustime/plugin_intellij/network/TestaustimeAPIClient.kt @@ -0,0 +1,69 @@ +package fi.testaustime.plugin_intellij.network + +import com.google.gson.Gson +import fi.testaustime.plugin_intellij.network.models.ActivityPostPayload +import java.net.HttpURLConnection +import java.net.URI +import java.net.URL +import java.net.http.HttpClient +import java.net.http.HttpRequest.BodyPublishers.noBody +import java.net.http.HttpRequest.BodyPublishers.ofString +import java.net.http.HttpRequest.newBuilder +import java.net.http.HttpResponse +import java.net.http.HttpResponse.BodyHandlers.discarding +import java.net.http.HttpResponse.BodyHandlers.ofString +import java.time.Duration.ofSeconds +import java.util.concurrent.CompletableFuture + +class TestaustimeAPIClient(private val baseUrl: String, token: String) { + private val headers = arrayOf( + "User-Agent", USER_AGENT, + "Content-Type", "application/json", + "Authorization", "Bearer $token" + ) + + fun flushActivity(): CompletableFuture> { + return HTTP_CLIENT.sendAsync( + newBuilder(URI("$baseUrl/activity/flush")) + .POST(noBody()).headers(*headers).build(), discarding() + ) + } + + fun activityLog(payload: ActivityPostPayload): CompletableFuture> { + return HTTP_CLIENT.sendAsync( + newBuilder(URI("$baseUrl/activity/update")) + .POST(ofString(Gson().toJson(payload))) + .headers(*headers).build(), + ofString() + ); + } + + fun me(): CompletableFuture> { + return HTTP_CLIENT.sendAsync( + newBuilder(URI("$baseUrl/users/@me")).GET().headers(*headers).build(), + ofString() + ); + } + + companion object { + const val USER_AGENT = "Intellij-Testaustime" + val HTTP_CLIENT: HttpClient = HttpClient.newBuilder() + .connectTimeout(ofSeconds(10L)).build(); + + fun verifyToken(baseUrl: String, token: String): Boolean { + try { + val conn: HttpURLConnection = URL(baseUrl).openConnection() as HttpURLConnection + conn.disconnect() + } catch (ex: Exception) { + return true; + } + val request = newBuilder(URI("$baseUrl/users/@me")).GET().headers( + "Authorization", "Bearer $token", + "User-Agent", USER_AGENT + ).build() + val resp = HTTP_CLIENT.send(request, discarding()); + return resp.statusCode() != 401; + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/fi/testaustime/plugin_intellij/services/TestausTimeApplicationService.kt b/src/main/kotlin/fi/testaustime/plugin_intellij/services/TestausTimeApplicationService.kt deleted file mode 100644 index aa12d2b..0000000 --- a/src/main/kotlin/fi/testaustime/plugin_intellij/services/TestausTimeApplicationService.kt +++ /dev/null @@ -1,12 +0,0 @@ -package fi.testaustime.plugin_intellij.services - -import com.intellij.openapi.diagnostic.Logger -import fi.testaustime.plugin_intellij.TestausTimeBundle -import org.jetbrains.rpc.LOG - -class TestausTimeApplicationService { - - init { - Logger.getInstance("#Testaustime").debug(TestausTimeBundle.message("applicationService")) - } -} diff --git a/src/main/kotlin/fi/testaustime/plugin_intellij/services/TestausTimeProjectService.kt b/src/main/kotlin/fi/testaustime/plugin_intellij/services/TestausTimeProjectService.kt deleted file mode 100644 index 1e3ed9e..0000000 --- a/src/main/kotlin/fi/testaustime/plugin_intellij/services/TestausTimeProjectService.kt +++ /dev/null @@ -1,88 +0,0 @@ -package fi.testaustime.plugin_intellij.services - -import com.intellij.ide.DataManager -import com.intellij.openapi.actionSystem.PlatformDataKeys -import com.intellij.openapi.application.ApplicationInfo -import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.project.Project -import com.intellij.psi.util.PsiUtilBase -import com.intellij.util.concurrency.AppExecutorUtil -import fi.testaustime.plugin_intellij.TestausTimeBundle -import fi.testaustime.plugin_intellij.configuration.TestausTimeSettingsState -import fi.testaustime.plugin_intellij.network.TestausTimeApiClient -import fi.testaustime.plugin_intellij.network.models.ActivityPostPayload -import fi.testaustime.plugin_intellij.utils.TestausTimeNotifier -import okhttp3.Callback -import java.net.InetAddress -import java.util.concurrent.ScheduledFuture -import java.util.concurrent.TimeUnit - - -class TestausTimeProjectService(private val project: Project) { - - private var scheduledPingTask: ScheduledFuture<*>? = null; - - init { - Logger.getInstance("#Testaustime").debug(TestausTimeBundle.message("projectService", project.name)) - startScheduledPinger(); - } - - fun terminateService() { - Logger.getInstance("#Testaustime").debug("Service terminated") - scheduledPingTask?.cancel(true) - } - - private fun startScheduledPinger() { - Logger.getInstance("#Testaustime").debug("Call for schedule ping registration") - if (scheduledPingTask != null) return - val settings = TestausTimeSettingsState.instance; - scheduledPingTask = AppExecutorUtil.getAppScheduledExecutorService().scheduleWithFixedDelay({ - try { - val hostname = InetAddress.getLocalHost().hostName - val appName = ApplicationInfo.getInstance().fullApplicationName - val dataContext = DataManager.getInstance().dataContextFromFocusAsync - dataContext.onSuccess { ctx -> - val project: Project? = ctx.getData(PlatformDataKeys.PROJECT) - if (project?.isOpen == true) { - if (settings.authToken.isBlank()) { - // Notify - TestausTimeNotifier.notifyWarning(project, "Authentication token is not configured!\nPlease configure in plugin settings") - scheduledPingTask?.cancel(false) - } else { - val editor: Editor? = ctx.getData(PlatformDataKeys.EDITOR) - var type: String? - val projectName = project.name - // Active editor is required - if (editor != null) { - val settings = TestausTimeSettingsState.instance; - val client = TestausTimeApiClient(settings.apiBaseUrl, settings.authToken); - type = PsiUtilBase.getPsiFileInEditor(editor, project)?.fileType?.displayName - println(type) - println(projectName) - println(appName) - println(hostname) - val resp = client.activityLog(ActivityPostPayload( - programmingLanguage = type, - projectName = projectName, - IDEName = appName, - host = hostname - )).execute() - Logger.getInstance("#Testaustime").debug(resp.message) - Logger.getInstance("#Testaustime").debug(resp.code.toString()) - Logger.getInstance("#Testaustime").debug(resp.body?.string()) - } - } - } - } - dataContext.onError { err -> - Logger.getInstance("#Testaustime").error(err) - } - } catch (e: Exception) { - println(e.message) - } - - }, 0, 30, TimeUnit.SECONDS) - Logger.getInstance("#Testaustime").debug("ScheduledPinger registered!") - } -} diff --git a/src/main/kotlin/fi/testaustime/plugin_intellij/services/TestaustimeApplicationService.kt b/src/main/kotlin/fi/testaustime/plugin_intellij/services/TestaustimeApplicationService.kt new file mode 100644 index 0000000..0b62b30 --- /dev/null +++ b/src/main/kotlin/fi/testaustime/plugin_intellij/services/TestaustimeApplicationService.kt @@ -0,0 +1,138 @@ +package fi.testaustime.plugin_intellij.services + +import com.intellij.ide.DataManager +import com.intellij.notification.NotificationType.ERROR +import com.intellij.openapi.actionSystem.PlatformDataKeys +import com.intellij.openapi.application.ApplicationInfo +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.util.PsiUtilBase +import com.intellij.util.concurrency.AppExecutorUtil +import fi.testaustime.plugin_intellij.TestaustimeBundle +import fi.testaustime.plugin_intellij.TestaustimeBundle.message +import fi.testaustime.plugin_intellij.configuration.SettingsState +import fi.testaustime.plugin_intellij.network.TestaustimeAPIClient +import fi.testaustime.plugin_intellij.network.models.ActivityPostPayload +import fi.testaustime.plugin_intellij.utils.TestaustimeNotifier +import java.net.InetAddress +import java.net.http.HttpResponse +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit + +class TestaustimeApplicationService { + + init { + Logger.getInstance("#Testaustime").debug(message("applicationService.started")) + startScheduledPinger(); + } + + private var scheduledPingTask: ScheduledFuture<*>? = null; + + // Track last project to know when to flush sessions + private var lastProject: Project? = null; + + // API token invalidation state, used for notification tracking + private var wasInvalid: Boolean = true; + private var isInvalid: Boolean = false; + + // Past connection exception state, used for de-duping connection exception notifications + private var didFail: Boolean = false; + + fun terminateService() { + flushActivity(); + scheduledPingTask?.cancel(true) + Logger.getInstance("#Testaustime").debug(message("applicationService.terminated")) + } + + private fun flushActivity() { + val settings = SettingsState.instance; + val client = TestaustimeAPIClient(settings.apiBaseUrl, settings.authToken); + client.flushActivity().join(); + } + + private fun startScheduledPinger() { + Logger.getInstance("#Testaustime").debug("Call for schedule ping registration") + if (scheduledPingTask != null) return + scheduledPingTask = AppExecutorUtil.getAppScheduledExecutorService().scheduleWithFixedDelay({ + pingNow() + }, 0, 30, TimeUnit.SECONDS) + Logger.getInstance("#Testaustime").debug( + message("applicationService.pinger.registered") + ) + } + + fun pingNow() { + val settings = SettingsState.instance; + + try { + val client = TestaustimeAPIClient(settings.apiBaseUrl, settings.authToken); + + val hostname = InetAddress.getLocalHost().hostName + val appName = ApplicationInfo.getInstance().fullApplicationName + val dataContext = DataManager.getInstance().dataContextFromFocusAsync + dataContext.onSuccess { ctx -> + if (isInvalid) { + wasInvalid = true; + return@onSuccess; + } + + ctx.getData(PlatformDataKeys.PROJECT)?.let { project -> + // Consider switching projects to be a new session + if (lastProject?.equals(project) == false) { + flushActivity() + lastProject = project + } + + val editor: Editor? = ctx.getData(PlatformDataKeys.EDITOR) + val type: String? + + val future: CompletableFuture> + if (project.isOpen && editor != null && settings.authToken.isNotBlank()) { + type = PsiUtilBase.getPsiFileInEditor(editor, project)?.fileType?.displayName + future = client.activityLog( + ActivityPostPayload( + programmingLanguage = type, + projectName = project.name, + IDEName = appName, + host = hostname + ) + ); + } else { + future = client.me(); + } + + future.thenAccept { resp -> + if (resp.statusCode() == 401) { + TestaustimeProjectService.broadcast(false) + isInvalid = true + wasInvalid = true + return@thenAccept + } else if (wasInvalid || didFail) { + TestaustimeProjectService.broadcast(true) + wasInvalid = false + didFail = false; + } + + }.exceptionally { + if (!didFail) { + TestaustimeNotifier.notification( + ERROR, null, + message("applicationService.heartbeat.failed"), it.localizedMessage + ) + didFail = true; + } + return@exceptionally null; + } + } + } + dataContext.onError { err -> + Logger.getInstance("#Testaustime").error(err) + } + } catch (e: Exception) { + Logger.getInstance("#Testaustime").error(e) + } + } +} diff --git a/src/main/kotlin/fi/testaustime/plugin_intellij/services/TestaustimeProjectService.kt b/src/main/kotlin/fi/testaustime/plugin_intellij/services/TestaustimeProjectService.kt new file mode 100644 index 0000000..d919e59 --- /dev/null +++ b/src/main/kotlin/fi/testaustime/plugin_intellij/services/TestaustimeProjectService.kt @@ -0,0 +1,32 @@ +package fi.testaustime.plugin_intellij.services + +import com.intellij.notification.NotificationType +import com.intellij.openapi.project.Project +import fi.testaustime.plugin_intellij.TestaustimeBundle.message +import fi.testaustime.plugin_intellij.utils.TestaustimeNotifier + + +class TestaustimeProjectService(private val project: Project) { + init { + projects.add(project); + } + + fun terminate() { + projects.remove(project); + } + + companion object { + var projects: MutableList = ArrayList(); + fun broadcast(tokenValid: Boolean) { + for (project in projects) { + if (tokenValid) { + TestaustimeNotifier.notifyInfo(project, message("projectService.active.title"), + message("projectService.active.message", project.name)); + } else { + TestaustimeNotifier.notification(NotificationType.ERROR, project, message("projectService.invalid.title"), + message("projectService.invalid.message", project.name)) + } + } + } + } +} diff --git a/src/main/kotlin/fi/testaustime/plugin_intellij/utils/TestausTimeNotifier.kt b/src/main/kotlin/fi/testaustime/plugin_intellij/utils/TestausTimeNotifier.kt deleted file mode 100644 index 5e7aacc..0000000 --- a/src/main/kotlin/fi/testaustime/plugin_intellij/utils/TestausTimeNotifier.kt +++ /dev/null @@ -1,19 +0,0 @@ -package fi.testaustime.plugin_intellij.utils - -import com.intellij.notification.NotificationGroupManager -import com.intellij.notification.NotificationType -import com.intellij.openapi.project.Project -import org.jetbrains.annotations.Nullable - - -object TestausTimeNotifier { - fun notifyWarning( - @Nullable project: Project?, - content: String? - ) { - NotificationGroupManager.getInstance() - .getNotificationGroup("TestausTime Notifications") - .createNotification("Testaustime", content!!, NotificationType.WARNING) - .notify(project) - } -} \ No newline at end of file diff --git a/src/main/kotlin/fi/testaustime/plugin_intellij/utils/TestaustimeNotifier.kt b/src/main/kotlin/fi/testaustime/plugin_intellij/utils/TestaustimeNotifier.kt new file mode 100644 index 0000000..8716964 --- /dev/null +++ b/src/main/kotlin/fi/testaustime/plugin_intellij/utils/TestaustimeNotifier.kt @@ -0,0 +1,45 @@ +package fi.testaustime.plugin_intellij.utils + +import com.intellij.notification.NotificationGroupManager +import com.intellij.notification.NotificationType +import com.intellij.notification.NotificationType.INFORMATION +import com.intellij.openapi.project.Project +import org.jetbrains.annotations.Nullable + + +object TestaustimeNotifier { + fun notifyWarning( + @Nullable project: Project?, + title: String, + content: String + ) { + NotificationGroupManager.getInstance() + .getNotificationGroup("testaustime.warnings") + .createNotification(title, content, NotificationType.WARNING) + .notify(project) + } + + fun notifyInfo( + project: Project?, + title: String, + content: String + ) { + NotificationGroupManager.getInstance() + .getNotificationGroup("testaustime.info") + .createNotification(title, content, INFORMATION) + .notify(project) + } + + fun notification( + type: NotificationType, + project: Project?, + title: String, + content: String + ) { + NotificationGroupManager.getInstance() + .getNotificationGroup(if (type == INFORMATION) + "testaustime.info" else "testaustime.warnings") + .createNotification(title, content, type) + .notify(project) + } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 65d2e6d..345442b 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -4,7 +4,6 @@ Testaustime com.intellij.modules.platform - - - - - + + + + + + key="notificationGroup.warnings" bundle="messages.TestaustimeBundle"/> - + + diff --git a/src/main/resources/messages/MyBundle.properties b/src/main/resources/messages/MyBundle.properties deleted file mode 100644 index 78dbb24..0000000 --- a/src/main/resources/messages/MyBundle.properties +++ /dev/null @@ -1,3 +0,0 @@ -name=My Plugin -applicationService=Application service -projectService=Project service: {0} diff --git a/src/main/resources/messages/TestaustimeBundle.properties b/src/main/resources/messages/TestaustimeBundle.properties new file mode 100644 index 0000000..9083e20 --- /dev/null +++ b/src/main/resources/messages/TestaustimeBundle.properties @@ -0,0 +1,23 @@ +name = Testaustime + +applicationService.started = Started application service +applicationService.terminated = Application service terminated +notificationGroup.information = Testaustime information +notificationGroup.warnings = Testaustime warnings +applicationService.pinger.registered = Scheduler Pinger registered. +applicationService.heartbeat.failed = Testaustime heartbeat failed! + +settings.apiBaseURL = Testaustime API base URL +settings.apiBaseURL.noSlash = The API base URL must not end in a slash: it will be added automatically. +settings.apiBaseURL.mustBeURL = The API base URL must be a valid URL. + +settings.apiToken = Testaustime API token +settings.apiToken.needToken = Testaustime needs an API token to operate. +settings.apiToken.mustBeAlphanumeric = The API token must be alphanumeric. +settings.apiToken.invalidLength = The API token must have a length of exactly 32 characters (currently {0}) +settings.apiToken.invalid = {0} reports that this API token is invalid. + +projectService.active.title = Testaustime is active! +projectService.active.message = Testaustime is tracking your time spent coding {0}! :) +projectService.invalid.title = Testaustime API token invalid! +projectService.invalid.message = Testaustime is not tracking your time spent coding {0}... :( diff --git a/src/test/kotlin/fi/testaustime/plugin_intellij/MyPluginTest.kt b/src/test/kotlin/fi/testaustime/plugin_intellij/MyPluginTest.kt deleted file mode 100644 index 83fcf95..0000000 --- a/src/test/kotlin/fi/testaustime/plugin_intellij/MyPluginTest.kt +++ /dev/null @@ -1,31 +0,0 @@ -package fi.testaustime.plugin_intellij - -import com.intellij.ide.highlighter.XmlFileType -import com.intellij.psi.xml.XmlFile -import com.intellij.testFramework.TestDataPath -import com.intellij.testFramework.fixtures.BasePlatformTestCase -import com.intellij.util.PsiErrorElementUtil - -@TestDataPath("\$CONTENT_ROOT/src/test/testData") -class MyPluginTest : BasePlatformTestCase() { - - fun testXMLFile() { - val psiFile = myFixture.configureByText(XmlFileType.INSTANCE, "bar") - val xmlFile = assertInstanceOf(psiFile, XmlFile::class.java) - - assertFalse(PsiErrorElementUtil.hasErrors(project, xmlFile.virtualFile)) - - assertNotNull(xmlFile.rootTag) - - xmlFile.rootTag?.let { - assertEquals("foo", it.name) - assertEquals("bar", it.value.text) - } - } - - override fun getTestDataPath() = "src/test/testData/rename" - - fun testRename() { - myFixture.testRename("foo.xml", "foo_after.xml", "a2") - } -} diff --git a/src/test/testData/rename/foo.xml b/src/test/testData/rename/foo.xml deleted file mode 100644 index b21e9f2..0000000 --- a/src/test/testData/rename/foo.xml +++ /dev/null @@ -1,3 +0,0 @@ - - 1>Foo - diff --git a/src/test/testData/rename/foo_after.xml b/src/test/testData/rename/foo_after.xml deleted file mode 100644 index 980ca96..0000000 --- a/src/test/testData/rename/foo_after.xml +++ /dev/null @@ -1,3 +0,0 @@ - - Foo - From 4d331b4a78cf18a4bc3d6bddd68bb3be53391c56 Mon Sep 17 00:00:00 2001 From: Developer From Jokela <40487414+developerfromjokela@users.noreply.github.com> Date: Thu, 4 Aug 2022 11:04:18 +0300 Subject: [PATCH 2/5] Create CHANGELOG.md --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c4b4525 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ + + +# testaustime-intellij Changelog + +## 0.2.1 +### Added +- Input validation +- Code optimisation +- QOL changes From 05c889196e86de520318bea58f9310dd0fc895fd Mon Sep 17 00:00:00 2001 From: Developer From Jokela <40487414+developerfromjokela@users.noreply.github.com> Date: Thu, 4 Aug 2022 11:07:15 +0300 Subject: [PATCH 3/5] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4b4525..1184fc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ # testaustime-intellij Changelog -## 0.2.1 +## [UNRELEASED] ### Added - Input validation - Code optimisation From 42f603bf8fa398dd215a8906140edf971838f8dd Mon Sep 17 00:00:00 2001 From: Developer From Jokela <40487414+developerfromjokela@users.noreply.github.com> Date: Thu, 4 Aug 2022 11:09:49 +0300 Subject: [PATCH 4/5] Update CHANGELOG.md --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1184fc0..932ca64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,12 @@ # testaustime-intellij Changelog -## [UNRELEASED] +## [0.2.0] ### Added - Input validation - Code optimisation - QOL changes + +## [Unreleased] +### Added +- Release From fb2491e485c0a9082f10946dd6ee7e12a402c14c Mon Sep 17 00:00:00 2001 From: bluelhf Date: Thu, 4 Aug 2022 11:59:55 +0300 Subject: [PATCH 5/5] feat: re-add CHANGELOG.md --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a66ec5e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,22 @@ +## Version 0.2.0 + +Version 0.2.0 comes with numerous small quality-of-life +improvements and fixes. +- Renamed some elements to align with the Testaustime naming style. + - The settings page is now at Settings > Tools > Testaustime + - All messages that previously used the incorrect capitalisation, TestausTime, + have been updated to the correct capitalisation. +- Added input verification to the settings page. + - The settings page will now notify the user of incorrect URLs or invalid tokens. It will + not be possible to apply changes to settings unless both the API base URL and API token + are valid. A blank token is allowed, however, to temporarily disable the API. +- Fixed multi-project tracking + - Previously, if multiple projects were open, the plugin would + only send heartbeats to the API for one project, and it would send + disproportionally many of them. This is now fixed. +- Improved notifications + - Notifications are now grouped into Testaustime Information and Testaustime Warnings. + These may be adjusted in the IDE's notification settings. + +> Warning +> Changelog unavailable for versions below 0.2.0. \ No newline at end of file